feat(deploy): single-origin path-based deployment + project site #34

Merged
developer merged 4 commits from feature/deploy-single-origin into development 2026-05-23 17:24:25 +00:00
104 changed files with 2967 additions and 787 deletions
Showing only changes of commit 8565942392 - Show all commits
+1 -1
View File
@@ -28,4 +28,4 @@ jobs:
echo " 2. scp the .tar.gz bundles to the production host." echo " 2. scp the .tar.gz bundles to the production host."
echo " 3. ssh prod 'docker load -i ...' for backend / gateway / engine." 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 " 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://<public host>/healthz and roll back on failure."
+28 -4
View File
@@ -24,6 +24,7 @@ on:
- 'game/**' - 'game/**'
- 'pkg/**' - 'pkg/**'
- 'ui/**' - 'ui/**'
- 'site/**'
- 'go.work' - 'go.work'
- 'go.work.sum' - 'go.work.sum'
- 'tools/dev-deploy/**' - 'tools/dev-deploy/**'
@@ -76,7 +77,11 @@ jobs:
- name: Build UI frontend - name: Build UI frontend
working-directory: ui/frontend working-directory: ui/frontend
env: 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 # Surface the synthetic-report loader and similar dev-only
# affordances in the long-lived dev bundle. The prod build # affordances in the long-lived dev bundle. The prod build
# path (`prod-build.yaml`) leaves this flag unset so the # 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)" export VITE_GATEWAY_RESPONSE_PUBLIC_KEY="$(grep -E '^VITE_GATEWAY_RESPONSE_PUBLIC_KEY=' .env.development | cut -d= -f2)"
pnpm build 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 - name: Build galaxy-engine image
working-directory: ${{ gitea.workspace }} working-directory: ${{ gitea.workspace }}
run: | run: |
@@ -112,6 +125,14 @@ jobs:
-v "${{ gitea.workspace }}/ui/frontend/build:/src:ro" \ -v "${{ gitea.workspace }}/ui/frontend/build:/src:ro" \
alpine sh -c 'rm -rf /dst/* /dst/.??* 2>/dev/null; cp -a /src/. /dst/' 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 - name: Seed geoip volume
run: | run: |
# Copy the GeoIP test fixture into a named volume so the # Copy the GeoIP test fixture into a named volume so the
@@ -162,9 +183,12 @@ jobs:
# `tls internal`) terminates and forwards into the edge # `tls internal`) terminates and forwards into the edge
# network. We accept the host's internal CA via -k because # network. We accept the host's internal CA via -k because
# the runner image has no reason to trust it. # 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 | tee /tmp/healthz
test -s /tmp/healthz test -s /tmp/healthz
curl -sk --max-time 10 -o /dev/null -w '%{http_code}\n' \ curl -sk --max-time 10 -o /dev/null -w '%{http_code}\n' \
https://www.galaxy.lan/ | tee /tmp/www_status https://galaxy.lan/ | tee /tmp/site_status
grep -qE '^(200|304)$' /tmp/www_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
+16 -1
View File
@@ -16,6 +16,7 @@ on:
- 'game/**' - 'game/**'
- 'pkg/**' - 'pkg/**'
- 'ui/**' - 'ui/**'
- 'site/**'
- 'go.work' - 'go.work'
- 'go.work.sum' - 'go.work.sum'
- '.gitea/workflows/prod-build.yaml' - '.gitea/workflows/prod-build.yaml'
@@ -93,7 +94,11 @@ jobs:
- name: Build UI bundle - name: Build UI bundle
working-directory: ui/frontend working-directory: ui/frontend
env: 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: | run: |
# Production response-signing public key is not in the repo # Production response-signing public key is not in the repo
# yet (the dev key in `tools/local-dev/keys/` is for dev # 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)" export VITE_GATEWAY_RESPONSE_PUBLIC_KEY="$(grep -E '^VITE_GATEWAY_RESPONSE_PUBLIC_KEY=' .env.development | cut -d= -f2)"
pnpm build 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 - name: Save images as artifact bundles
run: | run: |
mkdir -p artifacts mkdir -p artifacts
@@ -115,6 +128,8 @@ jobs:
| gzip >"artifacts/game-engine-${{ steps.tag.outputs.tag }}.tar.gz" | gzip >"artifacts/game-engine-${{ steps.tag.outputs.tag }}.tar.gz"
tar -C ui/frontend -czf \ tar -C ui/frontend -czf \
"artifacts/ui-dist-${{ steps.tag.outputs.tag }}.tar.gz" build "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 - name: Upload images
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
+47
View File
@@ -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
+2 -1
View File
@@ -44,7 +44,8 @@ Branches:
is manual through `deploy-prod.yaml`. is manual through `deploy-prod.yaml`.
- `development` — long-lived dev integration branch. Every merge into - `development` — long-lived dev integration branch. Every merge into
it auto-deploys to the dev environment via `dev-deploy.yaml` 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 - `feature/*` — short-lived branches off `development`. Merged back
via PR; only then do they reach the dev environment automatically. via PR; only then do they reach the dev environment automatically.
+35 -9
View File
@@ -579,13 +579,25 @@ behaviour for any of its guarantees.
The authenticated edge listener is built on `connectrpc.com/connect` and The authenticated edge listener is built on `connectrpc.com/connect` and
natively serves the Connect, gRPC, and gRPC-Web protocols on a single natively serves the Connect, gRPC, and gRPC-Web protocols on a single
HTTP/2 cleartext (`h2c`) port. Browser clients use Connect via HTTP/2 cleartext (`h2c`) port. The v1 service is `edge.v1.Gateway`;
`@connectrpc/connect-web`; native iOS / Android / desktop clients can browser clients address its methods at `/rpc/edge.v1.Gateway/<Method>`
use either Connect or raw gRPC framing against the same listener. and the edge strips the `/rpc` prefix so the gateway sees the
Envelope, signature, freshness, and anti-replay rules below are proto-derived `/edge.v1.Gateway/<Method>` path. Browser clients use
protocol-agnostic — they apply identically to every supported wire 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. 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 ### Principles
- No browser cookies. - No browser cookies.
@@ -775,6 +787,9 @@ domain tables.
### TLS and MITM ### 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 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. Browser clients rely on browser-managed TLS and the
signed exchange. signed exchange.
@@ -845,8 +860,10 @@ Branches:
way in is a PR merge from `development`. way in is a PR merge from `development`.
- `development` — long-lived dev integration branch. Every merge - `development` — long-lived dev integration branch. Every merge
triggers an auto-deploy into the long-lived dev environment on the triggers an auto-deploy into the long-lived dev environment on the
CI host, reachable through the host Caddy at CI host, reachable through the host Caddy at a single origin
`https://www.galaxy.lan` and `https://api.galaxy.lan`. `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 - `feature/*` — short-lived branches off `development`. Merged back
via PR; PRs run unit + integration checks before merge. via PR; PRs run unit + integration checks before merge.
@@ -872,8 +889,9 @@ Environments:
- **`tools/local-dev/`** — single-developer playground. Bound to - **`tools/local-dev/`** — single-developer playground. Bound to
host ports, Vite dev server runs on the host. Not driven by CI. host ports, Vite dev server runs on the host. Not driven by CI.
- **`tools/dev-deploy/`** — long-lived dev environment behind - **`tools/dev-deploy/`** — long-lived dev environment behind the
`*.galaxy.lan`, redeployed on every merge into `development`. single origin `galaxy.lan`, redeployed on every merge into
`development`.
- **production** — future. Images come from the - **production** — future. Images come from the
`galaxy-images-commit-<sha>` artifact produced by `prod-build.yaml` `galaxy-images-commit-<sha>` artifact produced by `prod-build.yaml`
and are shipped to the production host via `docker save` and are shipped to the production host via `docker save`
@@ -913,6 +931,14 @@ untouched by compose between deploys.
## 19. Deployment Topology (informational) ## 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` - MVP runs three executables: one `gateway` instance, one `backend`
instance, and N `galaxy-game-{game_id}` containers managed by backend. instance, and N `galaxy-game-{game_id}` containers managed by backend.
- One Postgres database is shared by `backend` only. - One Postgres database is shared by `backend` only.
+20 -5
View File
@@ -94,8 +94,20 @@ The authenticated edge listener is built on
the Connect, gRPC, and gRPC-Web protocols on a single HTTP/2 cleartext the Connect, gRPC, and gRPC-Web protocols on a single HTTP/2 cleartext
(`h2c`) port. Browser clients use `@connectrpc/connect-web`; native (`h2c`) port. Browser clients use `@connectrpc/connect-web`; native
clients can use either Connect or raw gRPC framing against the same clients can use either Connect or raw gRPC framing against the same
listener. Production TLS termination happens upstream of the gateway, listener. TLS termination happens upstream of the gateway at the edge
matching the previous gRPC-only deployment posture. 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 ### 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. a signed service event that includes the current server time in milliseconds.
The v1 protobuf contract lives in The v1 protobuf contract lives in
`proto/galaxy/gateway/v1/edge_gateway.proto` under package `proto/edge/v1/edge_gateway.proto` under package `edge.v1` and service
`galaxy.gateway.v1` and service `EdgeGateway`. `Gateway`. Browser and native clients address its methods at
`/rpc/edge.v1.Gateway/<Method>`; the edge Caddy strips the `/rpc`
prefix so the gateway listener sees the proto-derived
`/edge.v1.Gateway/<Method>` path.
Generated Go bindings are committed under 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: handlers) and are regenerated with:
```bash ```bash
+8 -8
View File
@@ -11,7 +11,7 @@ import (
"galaxy/gateway/authn" "galaxy/gateway/authn"
"galaxy/gateway/internal/clock" "galaxy/gateway/internal/clock"
"galaxy/gateway/internal/downstream" "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"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -21,9 +21,9 @@ import (
// commandRoutingService translates the verified authenticated request context // commandRoutingService translates the verified authenticated request context
// into an internal downstream command and signs successful unary responses. // into an internal downstream command and signs successful unary responses.
type commandRoutingService struct { type commandRoutingService struct {
gatewayv1.UnimplementedEdgeGatewayServer edgev1.UnimplementedGatewayServer
subscribeDelegate gatewayv1.EdgeGatewayServer subscribeDelegate edgev1.GatewayServer
router downstream.Router router downstream.Router
responseSigner authn.ResponseSigner responseSigner authn.ResponseSigner
clock clock.Clock clock clock.Clock
@@ -32,7 +32,7 @@ type commandRoutingService struct {
// ExecuteCommand builds a verified downstream command, routes it by exact // ExecuteCommand builds a verified downstream command, routes it by exact
// message_type, executes it, and signs the resulting unary response. // 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) command, err := authenticatedCommandFromContext(ctx)
if err != nil { if err != nil {
return nil, err 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 nil, status.Error(codes.Unavailable, "response signer is unavailable")
} }
return &gatewayv1.ExecuteCommandResponse{ return &edgev1.ExecuteCommandResponse{
ProtocolVersion: command.ProtocolVersion, ProtocolVersion: command.ProtocolVersion,
RequestId: command.RequestID, RequestId: command.RequestID,
TimestampMs: responseTimestampMS, TimestampMs: responseTimestampMS,
@@ -93,13 +93,13 @@ func (s commandRoutingService) ExecuteCommand(ctx context.Context, _ *gatewayv1.
// SubscribeEvents delegates to the authenticated streaming service // SubscribeEvents delegates to the authenticated streaming service
// implementation selected during server construction. // 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) return s.subscribeDelegate.SubscribeEvents(req, stream)
} }
// newCommandRoutingService constructs the final authenticated service that // newCommandRoutingService constructs the final authenticated service that
// owns verified unary routing while preserving the delegated streaming path. // 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{ return commandRoutingService{
subscribeDelegate: subscribeDelegate, subscribeDelegate: subscribeDelegate,
router: router, router: router,
@@ -142,4 +142,4 @@ func (unavailableResponseSigner) SignEvent(authn.EventSigningFields) ([]byte, er
return nil, errors.New("response signer is unavailable") return nil, errors.New("response signer is unavailable")
} }
var _ gatewayv1.EdgeGatewayServer = commandRoutingService{} var _ edgev1.GatewayServer = commandRoutingService{}
+11 -11
View File
@@ -5,8 +5,8 @@ import (
"errors" "errors"
"fmt" "fmt"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"galaxy/gateway/proto/galaxy/gateway/v1/gatewayv1connect" "galaxy/gateway/proto/edge/v1/edgev1connect"
"connectrpc.com/connect" "connectrpc.com/connect"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -17,15 +17,15 @@ import (
// connectEdgeAdapter exposes the existing gRPC-shaped authenticated edge // connectEdgeAdapter exposes the existing gRPC-shaped authenticated edge
// service decorator stack (envelope → session → payload-hash → signature → // service decorator stack (envelope → session → payload-hash → signature →
// freshness/replay → rate-limit → routing/push) through the // 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 // own; the underlying decorator stack carries the full ingress contract
// unchanged. // unchanged.
type connectEdgeAdapter struct { type connectEdgeAdapter struct {
impl gatewayv1.EdgeGatewayServer impl edgev1.GatewayServer
} }
// newConnectEdgeAdapter wraps impl as a Connect handler. // 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} 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 // service, and wraps the typed response. gRPC `status.Error` values
// returned by the decorator stack are translated to *connect.Error so // returned by the decorator stack are translated to *connect.Error so
// the Connect client receives the matching code and message. // 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) resp, err := a.impl.ExecuteCommand(ctx, req.Msg)
if err != nil { if err != nil {
return nil, translateGRPCStatusError(err) 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 // stream; the remaining grpc.ServerStream surface is satisfied by no-op
// shims so the interface contract is met without panicking. Errors // shims so the interface contract is met without panicking. Errors
// returned by the decorator stack are translated to *connect.Error. // 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} wrapped := &connectEdgeStream{ctx: ctx, stream: stream}
if err := a.impl.SubscribeEvents(req.Msg, wrapped); err != nil { if err := a.impl.SubscribeEvents(req.Msg, wrapped); err != nil {
return translateGRPCStatusError(err) return translateGRPCStatusError(err)
@@ -83,19 +83,19 @@ func translateGRPCStatusError(err error) error {
return connect.NewError(connect.Code(grpcStatus.Code()), errors.New(grpcStatus.Message())) 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 // on top of *connect.ServerStream. The decorator stack reads the request
// context and pushes outbound events through Send; the rest of the // context and pushes outbound events through Send; the rest of the
// grpc.ServerStream surface is not exercised in the gateway, so the no-op // grpc.ServerStream surface is not exercised in the gateway, so the no-op
// implementations preserve the type contract without surprising behaviour. // implementations preserve the type contract without surprising behaviour.
type connectEdgeStream struct { type connectEdgeStream struct {
ctx context.Context ctx context.Context
stream *connect.ServerStream[gatewayv1.GatewayEvent] stream *connect.ServerStream[edgev1.GatewayEvent]
} }
// Send forwards a typed gateway event through the underlying Connect server // Send forwards a typed gateway event through the underlying Connect server
// stream. // stream.
func (s *connectEdgeStream) Send(event *gatewayv1.GatewayEvent) error { func (s *connectEdgeStream) Send(event *edgev1.GatewayEvent) error {
return s.stream.Send(event) 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 // SendMsg directly; if a future caller does, the typed Send path is used
// when the message is a GatewayEvent. // when the message is a GatewayEvent.
func (s *connectEdgeStream) SendMsg(m any) error { func (s *connectEdgeStream) SendMsg(m any) error {
event, ok := m.(*gatewayv1.GatewayEvent) event, ok := m.(*edgev1.GatewayEvent)
if !ok { if !ok {
return fmt.Errorf("connectEdgeStream.SendMsg: unsupported message type %T", m) return fmt.Errorf("connectEdgeStream.SendMsg: unsupported message type %T", m)
} }
+12 -12
View File
@@ -4,7 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"buf.build/go/protovalidate" "buf.build/go/protovalidate"
"google.golang.org/grpc" "google.golang.org/grpc"
@@ -47,14 +47,14 @@ func parsedEnvelopeFromContext(ctx context.Context) (parsedEnvelope, bool) {
// envelopeValidatingService applies envelope parsing and the protocol gate // envelopeValidatingService applies envelope parsing and the protocol gate
// before delegating to the configured service implementation. // before delegating to the configured service implementation.
type envelopeValidatingService struct { type envelopeValidatingService struct {
gatewayv1.UnimplementedEdgeGatewayServer edgev1.UnimplementedGatewayServer
delegate gatewayv1.EdgeGatewayServer delegate edgev1.GatewayServer
} }
// ExecuteCommand validates req and only then forwards it to the configured // ExecuteCommand validates req and only then forwards it to the configured
// delegate with the parsed envelope attached to ctx. // 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) envelope, err := parseExecuteCommandRequest(req)
if err != nil { if err != nil {
return nil, err 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 // SubscribeEvents validates req and only then forwards it to the configured
// delegate with the parsed envelope attached to the stream context. // 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) envelope, err := parseSubscribeEventsRequest(req)
if err != nil { if err != nil {
return err return err
@@ -79,7 +79,7 @@ func (s envelopeValidatingService) SubscribeEvents(req *gatewayv1.SubscribeEvent
// parseExecuteCommandRequest validates req according to the request-envelope // parseExecuteCommandRequest validates req according to the request-envelope
// rules and returns a cloned parsed envelope suitable for later auth steps. // 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 { if req == nil {
return parsedEnvelope{}, newMalformedEnvelopeError("request envelope must not be 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 // parseSubscribeEventsRequest validates req according to the request-envelope
// rules and returns a cloned parsed envelope suitable for later auth steps. // 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 { if req == nil {
return parsedEnvelope{}, newMalformedEnvelopeError("request envelope must not be 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 // newEnvelopeValidatingService wraps delegate with the envelope-validation
// gate. // gate.
func newEnvelopeValidatingService(delegate gatewayv1.EdgeGatewayServer) gatewayv1.EdgeGatewayServer { func newEnvelopeValidatingService(delegate edgev1.GatewayServer) edgev1.GatewayServer {
return envelopeValidatingService{delegate: delegate} return envelopeValidatingService{delegate: delegate}
} }
// canonicalExecuteCommandValidationError maps any ExecuteCommand validation // canonicalExecuteCommandValidationError maps any ExecuteCommand validation
// failure into the stable canonical error chosen by field order. // failure into the stable canonical error chosen by field order.
func canonicalExecuteCommandValidationError(req *gatewayv1.ExecuteCommandRequest) error { func canonicalExecuteCommandValidationError(req *edgev1.ExecuteCommandRequest) error {
switch { switch {
case req.GetProtocolVersion() == "": case req.GetProtocolVersion() == "":
return newMalformedEnvelopeError("protocol_version must not be empty") return newMalformedEnvelopeError("protocol_version must not be empty")
@@ -162,7 +162,7 @@ func canonicalExecuteCommandValidationError(req *gatewayv1.ExecuteCommandRequest
// canonicalSubscribeEventsValidationError maps any SubscribeEvents validation // canonicalSubscribeEventsValidationError maps any SubscribeEvents validation
// failure into the stable canonical error chosen by field order. // failure into the stable canonical error chosen by field order.
func canonicalSubscribeEventsValidationError(req *gatewayv1.SubscribeEventsRequest) error { func canonicalSubscribeEventsValidationError(req *edgev1.SubscribeEventsRequest) error {
switch { switch {
case req.GetProtocolVersion() == "": case req.GetProtocolVersion() == "":
return newMalformedEnvelopeError("protocol_version must not be empty") return newMalformedEnvelopeError("protocol_version must not be empty")
@@ -198,7 +198,7 @@ func newUnsupportedProtocolVersionError(version string) error {
type parsedEnvelopeContextKey struct{} type parsedEnvelopeContextKey struct{}
type envelopeContextStream struct { type envelopeContextStream struct {
grpc.ServerStreamingServer[gatewayv1.GatewayEvent] grpc.ServerStreamingServer[edgev1.GatewayEvent]
ctx context.Context ctx context.Context
} }
@@ -210,4 +210,4 @@ func (s envelopeContextStream) Context() context.Context {
return s.ctx return s.ctx
} }
var _ gatewayv1.EdgeGatewayServer = envelopeValidatingService{} var _ edgev1.GatewayServer = envelopeValidatingService{}
+43 -43
View File
@@ -4,7 +4,7 @@ import (
"context" "context"
"testing" "testing"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -19,10 +19,10 @@ func TestParseExecuteCommandRequest(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
mutate func(*gatewayv1.ExecuteCommandRequest) mutate func(*edgev1.ExecuteCommandRequest)
wantCode codes.Code wantCode codes.Code
wantMessage string wantMessage string
assertValid func(*testing.T, *gatewayv1.ExecuteCommandRequest, parsedEnvelope) assertValid func(*testing.T, *edgev1.ExecuteCommandRequest, parsedEnvelope)
}{ }{
{ {
name: "nil request", name: "nil request",
@@ -31,7 +31,7 @@ func TestParseExecuteCommandRequest(t *testing.T) {
}, },
{ {
name: "empty protocol version", name: "empty protocol version",
mutate: func(req *gatewayv1.ExecuteCommandRequest) { mutate: func(req *edgev1.ExecuteCommandRequest) {
req.ProtocolVersion = "" req.ProtocolVersion = ""
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -39,7 +39,7 @@ func TestParseExecuteCommandRequest(t *testing.T) {
}, },
{ {
name: "empty device session id", name: "empty device session id",
mutate: func(req *gatewayv1.ExecuteCommandRequest) { mutate: func(req *edgev1.ExecuteCommandRequest) {
req.DeviceSessionId = "" req.DeviceSessionId = ""
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -47,7 +47,7 @@ func TestParseExecuteCommandRequest(t *testing.T) {
}, },
{ {
name: "empty message type", name: "empty message type",
mutate: func(req *gatewayv1.ExecuteCommandRequest) { mutate: func(req *edgev1.ExecuteCommandRequest) {
req.MessageType = "" req.MessageType = ""
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -55,7 +55,7 @@ func TestParseExecuteCommandRequest(t *testing.T) {
}, },
{ {
name: "zero timestamp", name: "zero timestamp",
mutate: func(req *gatewayv1.ExecuteCommandRequest) { mutate: func(req *edgev1.ExecuteCommandRequest) {
req.TimestampMs = 0 req.TimestampMs = 0
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -63,7 +63,7 @@ func TestParseExecuteCommandRequest(t *testing.T) {
}, },
{ {
name: "empty request id", name: "empty request id",
mutate: func(req *gatewayv1.ExecuteCommandRequest) { mutate: func(req *edgev1.ExecuteCommandRequest) {
req.RequestId = "" req.RequestId = ""
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -71,7 +71,7 @@ func TestParseExecuteCommandRequest(t *testing.T) {
}, },
{ {
name: "empty payload bytes", name: "empty payload bytes",
mutate: func(req *gatewayv1.ExecuteCommandRequest) { mutate: func(req *edgev1.ExecuteCommandRequest) {
req.PayloadBytes = nil req.PayloadBytes = nil
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -79,7 +79,7 @@ func TestParseExecuteCommandRequest(t *testing.T) {
}, },
{ {
name: "empty payload hash", name: "empty payload hash",
mutate: func(req *gatewayv1.ExecuteCommandRequest) { mutate: func(req *edgev1.ExecuteCommandRequest) {
req.PayloadHash = nil req.PayloadHash = nil
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -87,7 +87,7 @@ func TestParseExecuteCommandRequest(t *testing.T) {
}, },
{ {
name: "empty signature", name: "empty signature",
mutate: func(req *gatewayv1.ExecuteCommandRequest) { mutate: func(req *edgev1.ExecuteCommandRequest) {
req.Signature = nil req.Signature = nil
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -95,7 +95,7 @@ func TestParseExecuteCommandRequest(t *testing.T) {
}, },
{ {
name: "unsupported protocol version", name: "unsupported protocol version",
mutate: func(req *gatewayv1.ExecuteCommandRequest) { mutate: func(req *edgev1.ExecuteCommandRequest) {
req.ProtocolVersion = "v2" req.ProtocolVersion = "v2"
}, },
wantCode: codes.FailedPrecondition, wantCode: codes.FailedPrecondition,
@@ -104,7 +104,7 @@ func TestParseExecuteCommandRequest(t *testing.T) {
{ {
name: "valid request", name: "valid request",
wantCode: codes.OK, 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() t.Helper()
assert.Equal(t, supportedProtocolVersion, envelope.ProtocolVersion) assert.Equal(t, supportedProtocolVersion, envelope.ProtocolVersion)
@@ -138,7 +138,7 @@ func TestParseExecuteCommandRequest(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
var req *gatewayv1.ExecuteCommandRequest var req *edgev1.ExecuteCommandRequest
if tt.name != "nil request" { if tt.name != "nil request" {
req = newValidExecuteCommandRequest() req = newValidExecuteCommandRequest()
if tt.mutate != nil { if tt.mutate != nil {
@@ -166,10 +166,10 @@ func TestParseSubscribeEventsRequest(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
mutate func(*gatewayv1.SubscribeEventsRequest) mutate func(*edgev1.SubscribeEventsRequest)
wantCode codes.Code wantCode codes.Code
wantMessage string wantMessage string
assertValid func(*testing.T, *gatewayv1.SubscribeEventsRequest, parsedEnvelope) assertValid func(*testing.T, *edgev1.SubscribeEventsRequest, parsedEnvelope)
}{ }{
{ {
name: "nil request", name: "nil request",
@@ -178,7 +178,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) {
}, },
{ {
name: "empty protocol version", name: "empty protocol version",
mutate: func(req *gatewayv1.SubscribeEventsRequest) { mutate: func(req *edgev1.SubscribeEventsRequest) {
req.ProtocolVersion = "" req.ProtocolVersion = ""
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -186,7 +186,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) {
}, },
{ {
name: "empty device session id", name: "empty device session id",
mutate: func(req *gatewayv1.SubscribeEventsRequest) { mutate: func(req *edgev1.SubscribeEventsRequest) {
req.DeviceSessionId = "" req.DeviceSessionId = ""
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -194,7 +194,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) {
}, },
{ {
name: "empty message type", name: "empty message type",
mutate: func(req *gatewayv1.SubscribeEventsRequest) { mutate: func(req *edgev1.SubscribeEventsRequest) {
req.MessageType = "" req.MessageType = ""
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -202,7 +202,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) {
}, },
{ {
name: "zero timestamp", name: "zero timestamp",
mutate: func(req *gatewayv1.SubscribeEventsRequest) { mutate: func(req *edgev1.SubscribeEventsRequest) {
req.TimestampMs = 0 req.TimestampMs = 0
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -210,7 +210,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) {
}, },
{ {
name: "empty request id", name: "empty request id",
mutate: func(req *gatewayv1.SubscribeEventsRequest) { mutate: func(req *edgev1.SubscribeEventsRequest) {
req.RequestId = "" req.RequestId = ""
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -218,7 +218,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) {
}, },
{ {
name: "empty payload hash", name: "empty payload hash",
mutate: func(req *gatewayv1.SubscribeEventsRequest) { mutate: func(req *edgev1.SubscribeEventsRequest) {
req.PayloadHash = nil req.PayloadHash = nil
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -226,7 +226,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) {
}, },
{ {
name: "empty signature", name: "empty signature",
mutate: func(req *gatewayv1.SubscribeEventsRequest) { mutate: func(req *edgev1.SubscribeEventsRequest) {
req.Signature = nil req.Signature = nil
}, },
wantCode: codes.InvalidArgument, wantCode: codes.InvalidArgument,
@@ -234,7 +234,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) {
}, },
{ {
name: "unsupported protocol version", name: "unsupported protocol version",
mutate: func(req *gatewayv1.SubscribeEventsRequest) { mutate: func(req *edgev1.SubscribeEventsRequest) {
req.ProtocolVersion = "v2" req.ProtocolVersion = "v2"
}, },
wantCode: codes.FailedPrecondition, wantCode: codes.FailedPrecondition,
@@ -243,7 +243,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) {
{ {
name: "valid request with empty payload bytes", name: "valid request with empty payload bytes",
wantCode: codes.OK, 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() t.Helper()
assert.Empty(t, req.GetPayloadBytes()) assert.Empty(t, req.GetPayloadBytes())
@@ -260,7 +260,7 @@ func TestParseSubscribeEventsRequest(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
var req *gatewayv1.SubscribeEventsRequest var req *edgev1.SubscribeEventsRequest
if tt.name != "nil request" { if tt.name != "nil request" {
req = newValidSubscribeEventsRequest() req = newValidSubscribeEventsRequest()
if tt.mutate != nil { if tt.mutate != nil {
@@ -286,10 +286,10 @@ func TestParseSubscribeEventsRequest(t *testing.T) {
func TestEnvelopeValidatingServiceExecuteCommandRejectsInvalidRequestBeforeDelegate(t *testing.T) { func TestEnvelopeValidatingServiceExecuteCommandRejectsInvalidRequestBeforeDelegate(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
service := newEnvelopeValidatingService(delegate) service := newEnvelopeValidatingService(delegate)
_, err := service.ExecuteCommand(context.Background(), &gatewayv1.ExecuteCommandRequest{}) _, err := service.ExecuteCommand(context.Background(), &edgev1.ExecuteCommandRequest{})
require.Error(t, err) require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err)) assert.Equal(t, codes.InvalidArgument, status.Code(err))
@@ -299,10 +299,10 @@ func TestEnvelopeValidatingServiceExecuteCommandRejectsInvalidRequestBeforeDeleg
func TestEnvelopeValidatingServiceSubscribeEventsRejectsInvalidRequestBeforeDelegate(t *testing.T) { func TestEnvelopeValidatingServiceSubscribeEventsRejectsInvalidRequestBeforeDelegate(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
service := newEnvelopeValidatingService(delegate) service := newEnvelopeValidatingService(delegate)
err := service.SubscribeEvents(&gatewayv1.SubscribeEventsRequest{}, stubGatewayEventStream{}) err := service.SubscribeEvents(&edgev1.SubscribeEventsRequest{}, stubGatewayEventStream{})
require.Error(t, err) require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err)) assert.Equal(t, codes.InvalidArgument, status.Code(err))
@@ -313,15 +313,15 @@ func TestEnvelopeValidatingServiceExecuteCommandAttachesParsedEnvelope(t *testin
t.Parallel() t.Parallel()
want := newValidExecuteCommandRequest() want := newValidExecuteCommandRequest()
delegate := &recordingEdgeGatewayService{ delegate := &recordingGatewayService{
executeCommandFunc: func(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { executeCommandFunc: func(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) {
envelope, ok := parsedEnvelopeFromContext(ctx) envelope, ok := parsedEnvelopeFromContext(ctx)
require.True(t, ok) require.True(t, ok)
assert.Equal(t, want.GetRequestId(), envelope.RequestID) assert.Equal(t, want.GetRequestId(), envelope.RequestID)
assert.Equal(t, want.GetDeviceSessionId(), envelope.DeviceSessionID) assert.Equal(t, want.GetDeviceSessionId(), envelope.DeviceSessionID)
assert.Equal(t, want.GetMessageType(), envelope.MessageType) assert.Equal(t, want.GetMessageType(), envelope.MessageType)
assert.Equal(t, want.GetPayloadBytes(), envelope.PayloadBytes) assert.Equal(t, want.GetPayloadBytes(), envelope.PayloadBytes)
return &gatewayv1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil return &edgev1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil
}, },
} }
service := newEnvelopeValidatingService(delegate) service := newEnvelopeValidatingService(delegate)
@@ -337,8 +337,8 @@ func TestEnvelopeValidatingServiceSubscribeEventsAttachesParsedEnvelope(t *testi
t.Parallel() t.Parallel()
want := newValidSubscribeEventsRequest() want := newValidSubscribeEventsRequest()
delegate := &recordingEdgeGatewayService{ delegate := &recordingGatewayService{
subscribeEventsFunc: func(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { subscribeEventsFunc: func(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error {
envelope, ok := parsedEnvelopeFromContext(stream.Context()) envelope, ok := parsedEnvelopeFromContext(stream.Context())
require.True(t, ok) require.True(t, ok)
assert.Equal(t, want.GetRequestId(), envelope.RequestID) assert.Equal(t, want.GetRequestId(), envelope.RequestID)
@@ -357,25 +357,25 @@ func TestEnvelopeValidatingServiceSubscribeEventsAttachesParsedEnvelope(t *testi
assert.Equal(t, 1, delegate.subscribeCalls) assert.Equal(t, 1, delegate.subscribeCalls)
} }
type recordingEdgeGatewayService struct { type recordingGatewayService struct {
gatewayv1.UnimplementedEdgeGatewayServer edgev1.UnimplementedGatewayServer
executeCalls int executeCalls int
subscribeCalls int subscribeCalls int
executeCommandFunc func(context.Context, *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) executeCommandFunc func(context.Context, *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error)
subscribeEventsFunc func(*gatewayv1.SubscribeEventsRequest, grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) 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++ s.executeCalls++
if s.executeCommandFunc != nil { if s.executeCommandFunc != nil {
return s.executeCommandFunc(ctx, req) 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++ s.subscribeCalls++
if s.subscribeEventsFunc != nil { if s.subscribeEventsFunc != nil {
return s.subscribeEventsFunc(req, stream) return s.subscribeEventsFunc(req, stream)
@@ -389,7 +389,7 @@ type stubGatewayEventStream struct {
ctx context.Context ctx context.Context
} }
func (s stubGatewayEventStream) Send(*gatewayv1.GatewayEvent) error { func (s stubGatewayEventStream) Send(*edgev1.GatewayEvent) error {
return nil return nil
} }
+7 -7
View File
@@ -7,7 +7,7 @@ import (
"galaxy/gateway/internal/clock" "galaxy/gateway/internal/clock"
"galaxy/gateway/internal/replay" "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"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -19,9 +19,9 @@ const minimumReplayReservationTTL = time.Millisecond
// freshnessAndReplayService applies freshness and anti-replay checks after // freshnessAndReplayService applies freshness and anti-replay checks after
// client-signature verification and before later policy or routing steps run. // client-signature verification and before later policy or routing steps run.
type freshnessAndReplayService struct { type freshnessAndReplayService struct {
gatewayv1.UnimplementedEdgeGatewayServer edgev1.UnimplementedGatewayServer
delegate gatewayv1.EdgeGatewayServer delegate edgev1.GatewayServer
clock clock.Clock clock clock.Clock
replayStore replay.Store replayStore replay.Store
freshnessWindow time.Duration freshnessWindow time.Duration
@@ -29,7 +29,7 @@ type freshnessAndReplayService struct {
// ExecuteCommand verifies request freshness and replay protection before // ExecuteCommand verifies request freshness and replay protection before
// delegating to the configured service implementation. // 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 { if err := s.verifyFreshnessAndReplay(ctx); err != nil {
return nil, err return nil, err
} }
@@ -39,7 +39,7 @@ func (s freshnessAndReplayService) ExecuteCommand(ctx context.Context, req *gate
// SubscribeEvents verifies request freshness and replay protection before // SubscribeEvents verifies request freshness and replay protection before
// delegating to the configured service implementation. // 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 { if err := s.verifyFreshnessAndReplay(stream.Context()); err != nil {
return err return err
} }
@@ -49,7 +49,7 @@ func (s freshnessAndReplayService) SubscribeEvents(req *gatewayv1.SubscribeEvent
// newFreshnessAndReplayService wraps delegate with the freshness and replay // newFreshnessAndReplayService wraps delegate with the freshness and replay
// gate. // 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{ return freshnessAndReplayService{
delegate: delegate, delegate: delegate,
clock: clk, clock: clk,
@@ -92,4 +92,4 @@ func (unavailableReplayStore) Reserve(context.Context, string, string, time.Dura
return errors.New("replay store is unavailable") return errors.New("replay store is unavailable")
} }
var _ gatewayv1.EdgeGatewayServer = freshnessAndReplayService{} var _ edgev1.GatewayServer = freshnessAndReplayService{}
@@ -9,7 +9,7 @@ import (
"galaxy/gateway/internal/replay" "galaxy/gateway/internal/replay"
"galaxy/gateway/internal/session" "galaxy/gateway/internal/session"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"connectrpc.com/connect" "connectrpc.com/connect"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -40,7 +40,7 @@ func TestExecuteCommandRejectsStaleTimestamp(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, 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.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, 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) { func TestExecuteCommandRejectsReplay(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, 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) { func TestSubscribeEventsRejectsReplay(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, 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) { func TestExecuteCommandAllowsSameRequestIDAcrossDistinctSessions(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{ delegate := &recordingGatewayService{
executeCommandFunc: func(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { executeCommandFunc: func(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) {
return &gatewayv1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil return &edgev1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil
}, },
} }
@@ -196,8 +196,8 @@ func TestExecuteCommandAllowsSameRequestIDAcrossDistinctSessions(t *testing.T) {
func TestSubscribeEventsAllowsSameRequestIDAcrossDistinctSessions(t *testing.T) { func TestSubscribeEventsAllowsSameRequestIDAcrossDistinctSessions(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{ delegate := &recordingGatewayService{
subscribeEventsFunc: func(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { subscribeEventsFunc: func(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error {
return nil return nil
}, },
} }
@@ -238,7 +238,7 @@ func TestSubscribeEventsAllowsSameRequestIDAcrossDistinctSessions(t *testing.T)
func TestExecuteCommandRejectsReplayStoreUnavailable(t *testing.T) { func TestExecuteCommandRejectsReplayStoreUnavailable(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, 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) { func TestSubscribeEventsRejectsReplayStoreUnavailable(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, 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) { func TestExecuteCommandFreshRequestReachesDelegateAndUsesDynamicReplayTTL(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{ delegate := &recordingGatewayService{
executeCommandFunc: func(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { executeCommandFunc: func(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) {
return &gatewayv1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil return &edgev1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil
}, },
} }
@@ -324,8 +324,8 @@ func TestExecuteCommandFreshRequestReachesDelegateAndUsesDynamicReplayTTL(t *tes
func TestSubscribeEventsFreshRequestReachesDelegateAndUsesDynamicReplayTTL(t *testing.T) { func TestSubscribeEventsFreshRequestReachesDelegateAndUsesDynamicReplayTTL(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{ delegate := &recordingGatewayService{
subscribeEventsFunc: func(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { subscribeEventsFunc: func(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error {
return nil return nil
}, },
} }
@@ -361,9 +361,9 @@ func TestSubscribeEventsFreshRequestReachesDelegateAndUsesDynamicReplayTTL(t *te
func TestExecuteCommandFutureSkewUsesExtendedReplayTTL(t *testing.T) { func TestExecuteCommandFutureSkewUsesExtendedReplayTTL(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{ delegate := &recordingGatewayService{
executeCommandFunc: func(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { executeCommandFunc: func(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) {
return &gatewayv1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil return &edgev1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil
}, },
} }
@@ -395,9 +395,9 @@ func TestExecuteCommandFutureSkewUsesExtendedReplayTTL(t *testing.T) {
func TestExecuteCommandBoundaryFreshnessUsesMinimumReplayTTL(t *testing.T) { func TestExecuteCommandBoundaryFreshnessUsesMinimumReplayTTL(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{ delegate := &recordingGatewayService{
executeCommandFunc: func(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { executeCommandFunc: func(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) {
return &gatewayv1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil return &edgev1.ExecuteCommandResponse{RequestId: req.GetRequestId()}, nil
}, },
} }
+4 -4
View File
@@ -8,7 +8,7 @@ import (
"galaxy/gateway/internal/logging" "galaxy/gateway/internal/logging"
"galaxy/gateway/internal/telemetry" "galaxy/gateway/internal/telemetry"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.uber.org/zap" "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) { func envelopeFieldsFromRequest(req any) (messageType string, requestID string, traceID string) {
switch typed := req.(type) { switch typed := req.(type) {
case *gatewayv1.ExecuteCommandRequest: case *edgev1.ExecuteCommandRequest:
return typed.GetMessageType(), typed.GetRequestId(), typed.GetTraceId() return typed.GetMessageType(), typed.GetRequestId(), typed.GetTraceId()
case *gatewayv1.SubscribeEventsRequest: case *edgev1.SubscribeEventsRequest:
return typed.GetMessageType(), typed.GetRequestId(), typed.GetTraceId() return typed.GetMessageType(), typed.GetRequestId(), typed.GetTraceId()
default: default:
return "", "", "" return "", "", ""
@@ -88,7 +88,7 @@ func envelopeFieldsFromRequest(req any) (messageType string, requestID string, t
} }
func resultCodeFromResponse(resp any) string { func resultCodeFromResponse(resp any) string {
typed, ok := resp.(*gatewayv1.ExecuteCommandResponse) typed, ok := resp.(*edgev1.ExecuteCommandResponse)
if !ok { if !ok {
return "" return ""
} }
+7 -7
View File
@@ -5,7 +5,7 @@ import (
"errors" "errors"
"galaxy/gateway/authn" "galaxy/gateway/authn"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -15,14 +15,14 @@ import (
// payloadHashVerifyingService applies payload-hash verification after session // payloadHashVerifyingService applies payload-hash verification after session
// lookup and before any later auth or routing step runs. // lookup and before any later auth or routing step runs.
type payloadHashVerifyingService struct { type payloadHashVerifyingService struct {
gatewayv1.UnimplementedEdgeGatewayServer edgev1.UnimplementedGatewayServer
delegate gatewayv1.EdgeGatewayServer delegate edgev1.GatewayServer
} }
// ExecuteCommand verifies req payload integrity before delegating to the // ExecuteCommand verifies req payload integrity before delegating to the
// configured service implementation. // 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 { if err := verifyPayloadHash(ctx); err != nil {
return nil, err 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 // SubscribeEvents verifies req payload integrity before delegating to the
// configured service implementation. // 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 { if err := verifyPayloadHash(stream.Context()); err != nil {
return err return err
} }
@@ -42,7 +42,7 @@ func (s payloadHashVerifyingService) SubscribeEvents(req *gatewayv1.SubscribeEve
// newPayloadHashVerifyingService wraps delegate with the payload-hash // newPayloadHashVerifyingService wraps delegate with the payload-hash
// verification gate. // verification gate.
func newPayloadHashVerifyingService(delegate gatewayv1.EdgeGatewayServer) gatewayv1.EdgeGatewayServer { func newPayloadHashVerifyingService(delegate edgev1.GatewayServer) edgev1.GatewayServer {
return payloadHashVerifyingService{delegate: delegate} return payloadHashVerifyingService{delegate: delegate}
} }
@@ -63,4 +63,4 @@ func verifyPayloadHash(ctx context.Context) error {
} }
} }
var _ gatewayv1.EdgeGatewayServer = payloadHashVerifyingService{} var _ edgev1.GatewayServer = payloadHashVerifyingService{}
@@ -15,7 +15,7 @@ import (
func TestExecuteCommandRejectsPayloadHashWithInvalidLength(t *testing.T) { func TestExecuteCommandRejectsPayloadHashWithInvalidLength(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, 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) { func TestExecuteCommandRejectsPayloadHashMismatch(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, 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) { func TestSubscribeEventsRejectsPayloadHashWithInvalidLength(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, 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) { func TestSubscribeEventsRejectsPayloadHashMismatch(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }},
+7 -7
View File
@@ -11,7 +11,7 @@ import (
"galaxy/gateway/internal/logging" "galaxy/gateway/internal/logging"
"galaxy/gateway/internal/push" "galaxy/gateway/internal/push"
"galaxy/gateway/internal/telemetry" "galaxy/gateway/internal/telemetry"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"go.uber.org/zap" "go.uber.org/zap"
"google.golang.org/grpc" "google.golang.org/grpc"
@@ -22,7 +22,7 @@ import (
// NewFanOutPushStreamService constructs the authenticated SubscribeEvents tail // NewFanOutPushStreamService constructs the authenticated SubscribeEvents tail
// service that registers active streams in hub and forwards client-facing // service that registers active streams in hub and forwards client-facing
// events after the bootstrap event has been sent. // 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 { if responseSigner == nil {
responseSigner = unavailableResponseSigner{} responseSigner = unavailableResponseSigner{}
} }
@@ -44,7 +44,7 @@ func NewFanOutPushStreamService(hub *push.Hub, responseSigner authn.ResponseSign
// fanOutPushStreamService owns the post-bootstrap authenticated push-stream // fanOutPushStreamService owns the post-bootstrap authenticated push-stream
// lifecycle backed by the in-memory push hub. // lifecycle backed by the in-memory push hub.
type fanOutPushStreamService struct { type fanOutPushStreamService struct {
gatewayv1.UnimplementedEdgeGatewayServer edgev1.UnimplementedGatewayServer
hub *push.Hub hub *push.Hub
responseSigner authn.ResponseSigner responseSigner authn.ResponseSigner
@@ -54,7 +54,7 @@ type fanOutPushStreamService struct {
// SubscribeEvents registers the verified stream in the push hub and forwards // SubscribeEvents registers the verified stream in the push hub and forwards
// matching client-facing events until the stream ends. // 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()) binding, ok := authenticatedStreamBindingFromContext(stream.Context())
if !ok { if !ok {
return status.Error(codes.Internal, "authenticated request context is incomplete") 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() timestampMS := s.clock.Now().UTC().UnixMilli()
payloadHash := sha256.Sum256(event.PayloadBytes) 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 nil, status.Error(codes.Unavailable, "response signer is unavailable")
} }
return &gatewayv1.GatewayEvent{ return &edgev1.GatewayEvent{
EventType: event.EventType, EventType: event.EventType,
EventId: event.EventID, EventId: event.EventID,
TimestampMs: timestampMS, TimestampMs: timestampMS,
@@ -169,4 +169,4 @@ func mapSubscriptionOutcome(err error) telemetry.EdgeOutcome {
} }
} }
var _ gatewayv1.EdgeGatewayServer = fanOutPushStreamService{} var _ edgev1.GatewayServer = fanOutPushStreamService{}
+6 -6
View File
@@ -6,7 +6,7 @@ import (
"time" "time"
"galaxy/gateway/internal/telemetry" "galaxy/gateway/internal/telemetry"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"google.golang.org/grpc" "google.golang.org/grpc"
@@ -26,7 +26,7 @@ import (
// 15-second default a fully-idle stream costs ~840 KB/day per client; // 15-second default a fully-idle stream costs ~840 KB/day per client;
// see `docs/ARCHITECTURE.md` for the per-scale projection. // see `docs/ARCHITECTURE.md` for the per-scale projection.
type heartbeatingStream struct { type heartbeatingStream struct {
grpc.ServerStreamingServer[gatewayv1.GatewayEvent] grpc.ServerStreamingServer[edgev1.GatewayEvent]
interval time.Duration interval time.Duration
metrics *telemetry.Runtime metrics *telemetry.Runtime
@@ -43,7 +43,7 @@ type heartbeatingStream struct {
// the wrapping entirely; non-nil returns must have `Stop()` called once // the wrapping entirely; non-nil returns must have `Stop()` called once
// the stream lifecycle ends. // the stream lifecycle ends.
func newHeartbeatingStream( func newHeartbeatingStream(
inner grpc.ServerStreamingServer[gatewayv1.GatewayEvent], inner grpc.ServerStreamingServer[edgev1.GatewayEvent],
interval time.Duration, interval time.Duration,
metrics *telemetry.Runtime, metrics *telemetry.Runtime,
) *heartbeatingStream { ) *heartbeatingStream {
@@ -64,7 +64,7 @@ func newHeartbeatingStream(
// so the heartbeat goroutine waits a fresh interval before firing // so the heartbeat goroutine waits a fresh interval before firing
// again. A Send that succeeds means the transport just delivered real // again. A Send that succeeds means the transport just delivered real
// bytes; the silence window restarts from "now". // 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() s.sendMu.Lock()
defer s.sendMu.Unlock() defer s.sendMu.Unlock()
if err := s.ServerStreamingServer.Send(event); err != nil { 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 // EventType is left at its proto3 default so the wire frame stays as
// small as Connect framing allows. See `gatewayHeartbeatEventType` for // small as Connect framing allows. See `gatewayHeartbeatEventType` for
// the security rationale of leaving the event unsigned. // the security rationale of leaving the event unsigned.
func buildHeartbeatEvent() *gatewayv1.GatewayEvent { func buildHeartbeatEvent() *edgev1.GatewayEvent {
return &gatewayv1.GatewayEvent{EventType: gatewayHeartbeatEventType} return &edgev1.GatewayEvent{EventType: gatewayHeartbeatEventType}
} }
@@ -7,7 +7,7 @@ import (
"testing" "testing"
"time" "time"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -66,7 +66,7 @@ func TestHeartbeatingStreamRealSendResetsSilenceTimer(t *testing.T) {
defer ticker.Stop() defer ticker.Stop()
for range 6 { for range 6 {
<-ticker.C <-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) t.Errorf("real Send failed: %v", err)
return return
} }
@@ -134,16 +134,16 @@ func TestHeartbeatingStreamSendErrorPropagates(t *testing.T) {
require.NotNil(t, hb) require.NotNil(t, hb)
defer hb.Stop() defer hb.Stop()
err := hb.Send(&gatewayv1.GatewayEvent{EventType: "real.event"}) err := hb.Send(&edgev1.GatewayEvent{EventType: "real.event"})
require.ErrorIs(t, err, wantErr) require.ErrorIs(t, err, wantErr)
} }
// capturingStream is a minimal grpc.ServerStreamingServer that pushes // capturingStream is a minimal grpc.ServerStreamingServer that pushes
// every Send into a channel so tests can assert on the wire frame. // every Send into a channel so tests can assert on the wire frame.
type capturingStream struct { type capturingStream struct {
grpc.ServerStreamingServer[gatewayv1.GatewayEvent] grpc.ServerStreamingServer[edgev1.GatewayEvent]
events chan *gatewayv1.GatewayEvent events chan *edgev1.GatewayEvent
sendErr atomic.Pointer[errorBox] sendErr atomic.Pointer[errorBox]
} }
@@ -152,10 +152,10 @@ type errorBox struct{ err error }
func newCapturingStream(t *testing.T) *capturingStream { func newCapturingStream(t *testing.T) *capturingStream {
t.Helper() 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 { if box := s.sendErr.Load(); box != nil {
return box.err 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) SendMsg(any) error { return errors.New("capturingStream.SendMsg: unused") }
func (s *capturingStream) RecvMsg(any) error { return errors.New("capturingStream.RecvMsg: 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() t.Helper()
select { select {
+12 -12
View File
@@ -9,7 +9,7 @@ import (
"galaxy/gateway/authn" "galaxy/gateway/authn"
"galaxy/gateway/internal/clock" "galaxy/gateway/internal/clock"
"galaxy/gateway/internal/telemetry" "galaxy/gateway/internal/telemetry"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
gatewayfbs "galaxy/schema/fbs/gateway" gatewayfbs "galaxy/schema/fbs/gateway"
flatbuffers "github.com/google/flatbuffers/go" 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 // the tail performs and only emits a heartbeat when the silence window
// elapses; tails remain heartbeat-unaware. // elapses; tails remain heartbeat-unaware.
type authenticatedPushStreamService struct { type authenticatedPushStreamService struct {
gatewayv1.UnimplementedEdgeGatewayServer edgev1.UnimplementedGatewayServer
tailDelegate gatewayv1.EdgeGatewayServer tailDelegate edgev1.GatewayServer
responseSigner authn.ResponseSigner responseSigner authn.ResponseSigner
clock clock.Clock clock clock.Clock
heartbeatInterval time.Duration heartbeatInterval time.Duration
@@ -92,7 +92,7 @@ type authenticatedPushStreamService struct {
// SubscribeEvents binds the verified stream identity, sends the initial signed // SubscribeEvents binds the verified stream identity, sends the initial signed
// server-time event, and then delegates the remaining lifecycle. // 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()) envelope, ok := parsedEnvelopeFromContext(stream.Context())
if !ok { if !ok {
return status.Error(codes.Internal, "authenticated request context is incomplete") 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") return status.Error(codes.Unavailable, "response signer is unavailable")
} }
if err := boundStream.Send(&gatewayv1.GatewayEvent{ if err := boundStream.Send(&edgev1.GatewayEvent{
EventType: serverTimeEventType, EventType: serverTimeEventType,
EventId: envelope.RequestID, EventId: envelope.RequestID,
TimestampMs: serverTimeMS, TimestampMs: serverTimeMS,
@@ -147,7 +147,7 @@ func (s authenticatedPushStreamService) SubscribeEvents(req *gatewayv1.Subscribe
return err 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 { if hbStream := newHeartbeatingStream(boundStream, s.heartbeatInterval, s.metrics); hbStream != nil {
defer hbStream.Stop() defer hbStream.Stop()
go func() { go func() {
@@ -165,12 +165,12 @@ func (s authenticatedPushStreamService) SubscribeEvents(req *gatewayv1.Subscribe
} }
func newAuthenticatedPushStreamService( func newAuthenticatedPushStreamService(
tailDelegate gatewayv1.EdgeGatewayServer, tailDelegate edgev1.GatewayServer,
responseSigner authn.ResponseSigner, responseSigner authn.ResponseSigner,
clk clock.Clock, clk clock.Clock,
heartbeatInterval time.Duration, heartbeatInterval time.Duration,
metrics *telemetry.Runtime, metrics *telemetry.Runtime,
) gatewayv1.EdgeGatewayServer { ) edgev1.GatewayServer {
if tailDelegate == nil { if tailDelegate == nil {
tailDelegate = holdOpenSubscribeEventsService{} tailDelegate = holdOpenSubscribeEventsService{}
} }
@@ -197,7 +197,7 @@ func buildServerTimeEventPayload(serverTimeMS int64) []byte {
type authenticatedStreamBindingContextKey struct{} type authenticatedStreamBindingContextKey struct{}
type authenticatedStreamContextStream struct { type authenticatedStreamContextStream struct {
grpc.ServerStreamingServer[gatewayv1.GatewayEvent] grpc.ServerStreamingServer[edgev1.GatewayEvent]
ctx context.Context ctx context.Context
} }
@@ -210,12 +210,12 @@ func (s authenticatedStreamContextStream) Context() context.Context {
} }
type holdOpenSubscribeEventsService struct { 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() <-stream.Context().Done()
return stream.Context().Err() return stream.Context().Err()
} }
var _ gatewayv1.EdgeGatewayServer = authenticatedPushStreamService{} var _ edgev1.GatewayServer = authenticatedPushStreamService{}
+7 -7
View File
@@ -7,7 +7,7 @@ import (
"galaxy/gateway/internal/config" "galaxy/gateway/internal/config"
"galaxy/gateway/internal/ratelimit" "galaxy/gateway/internal/ratelimit"
"galaxy/gateway/internal/session" "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"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -102,9 +102,9 @@ type AuthenticatedRequestPolicy interface {
} }
type authenticatedRateLimitService struct { type authenticatedRateLimitService struct {
gatewayv1.UnimplementedEdgeGatewayServer edgev1.UnimplementedGatewayServer
delegate gatewayv1.EdgeGatewayServer delegate edgev1.GatewayServer
limiter AuthenticatedRequestLimiter limiter AuthenticatedRequestLimiter
policy AuthenticatedRequestPolicy policy AuthenticatedRequestPolicy
cfg config.AuthenticatedGRPCAntiAbuseConfig cfg config.AuthenticatedGRPCAntiAbuseConfig
@@ -112,7 +112,7 @@ type authenticatedRateLimitService struct {
// ExecuteCommand applies authenticated rate limits and edge policy before // ExecuteCommand applies authenticated rate limits and edge policy before
// delegating to the configured service implementation. // 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 { if err := s.applyRateLimitsAndPolicy(ctx, authenticatedRPCExecuteCommand); err != nil {
return nil, err return nil, err
} }
@@ -122,7 +122,7 @@ func (s authenticatedRateLimitService) ExecuteCommand(ctx context.Context, req *
// SubscribeEvents applies authenticated rate limits and edge policy before // SubscribeEvents applies authenticated rate limits and edge policy before
// delegating to the configured service implementation. // 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 { if err := s.applyRateLimitsAndPolicy(stream.Context(), authenticatedRPCSubscribeEvents); err != nil {
return err return err
} }
@@ -132,7 +132,7 @@ func (s authenticatedRateLimitService) SubscribeEvents(req *gatewayv1.SubscribeE
// newAuthenticatedRateLimitService wraps delegate with the authenticated // newAuthenticatedRateLimitService wraps delegate with the authenticated
// rate-limit and edge-policy gate. // 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{ return authenticatedRateLimitService{
delegate: delegate, delegate: delegate,
limiter: limiter, limiter: limiter,
@@ -279,4 +279,4 @@ func (noopAuthenticatedRequestPolicy) Evaluate(context.Context, AuthenticatedReq
return nil return nil
} }
var _ gatewayv1.EdgeGatewayServer = authenticatedRateLimitService{} var _ edgev1.GatewayServer = authenticatedRateLimitService{}
@@ -14,7 +14,7 @@ import (
"galaxy/gateway/internal/ratelimit" "galaxy/gateway/internal/ratelimit"
"galaxy/gateway/internal/restapi" "galaxy/gateway/internal/restapi"
"galaxy/gateway/internal/session" "galaxy/gateway/internal/session"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"connectrpc.com/connect" "connectrpc.com/connect"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -24,7 +24,7 @@ import (
func TestExecuteCommandRateLimitsByIP(t *testing.T) { func TestExecuteCommandRateLimitsByIP(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) { server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) {
cfg.AntiAbuse.IP = config.AuthenticatedRateLimitConfig{ cfg.AntiAbuse.IP = config.AuthenticatedRateLimitConfig{
Requests: 1, Requests: 1,
@@ -54,7 +54,7 @@ func TestExecuteCommandRateLimitsByIP(t *testing.T) {
func TestExecuteCommandRateLimitsBySession(t *testing.T) { func TestExecuteCommandRateLimitsBySession(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) { server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) {
cfg.AntiAbuse.Session = config.AuthenticatedRateLimitConfig{ cfg.AntiAbuse.Session = config.AuthenticatedRateLimitConfig{
Requests: 1, Requests: 1,
@@ -87,7 +87,7 @@ func TestExecuteCommandRateLimitsBySession(t *testing.T) {
func TestExecuteCommandRateLimitsByUser(t *testing.T) { func TestExecuteCommandRateLimitsByUser(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) { server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) {
cfg.AntiAbuse.User = config.AuthenticatedRateLimitConfig{ cfg.AntiAbuse.User = config.AuthenticatedRateLimitConfig{
Requests: 1, Requests: 1,
@@ -124,7 +124,7 @@ func TestExecuteCommandRateLimitsByUser(t *testing.T) {
func TestExecuteCommandRateLimitsByMessageClass(t *testing.T) { func TestExecuteCommandRateLimitsByMessageClass(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) { server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) {
cfg.AntiAbuse.MessageClass = config.AuthenticatedRateLimitConfig{ cfg.AntiAbuse.MessageClass = config.AuthenticatedRateLimitConfig{
Requests: 1, Requests: 1,
@@ -161,7 +161,7 @@ func TestAuthenticatedPolicyHookReceivesVerifiedRequest(t *testing.T) {
t.Parallel() t.Parallel()
policy := &recordingAuthenticatedRequestPolicy{} policy := &recordingAuthenticatedRequestPolicy{}
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: userMappedSessionCache(map[string]string{"device-session-123": "user-123"}), SessionCache: userMappedSessionCache(map[string]string{"device-session-123": "user-123"}),
@@ -189,7 +189,7 @@ func TestAuthenticatedPolicyHookReceivesVerifiedRequest(t *testing.T) {
func TestExecuteCommandPolicyRejectMapsToPermissionDenied(t *testing.T) { func TestExecuteCommandPolicyRejectMapsToPermissionDenied(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: userMappedSessionCache(map[string]string{"device-session-123": "user-123"}), SessionCache: userMappedSessionCache(map[string]string{"device-session-123": "user-123"}),
@@ -212,7 +212,7 @@ func TestExecuteCommandPolicyRejectMapsToPermissionDenied(t *testing.T) {
func TestSubscribeEventsRateLimitRejectsStream(t *testing.T) { func TestSubscribeEventsRateLimitRejectsStream(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) { server, runGateway := newTestGatewayWithGRPCConfig(t, newAuthenticatedGRPCConfigForTest(func(cfg *config.AuthenticatedGRPCConfig) {
cfg.AntiAbuse.IP = config.AuthenticatedRateLimitConfig{ cfg.AntiAbuse.IP = config.AuthenticatedRateLimitConfig{
Requests: 1, Requests: 1,
@@ -274,7 +274,7 @@ func TestAuthenticatedRateLimitsStayIsolatedFromPublicREST(t *testing.T) {
AuthService: staticAuthServiceClient{}, AuthService: staticAuthServiceClient{},
Limiter: publicLimiterAdapter{limiter: sharedLimiter}, Limiter: publicLimiterAdapter{limiter: sharedLimiter},
}) })
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
grpcServer := NewServer(grpcCfg, ServerDependencies{ grpcServer := NewServer(grpcCfg, ServerDependencies{
Service: delegate, Service: delegate,
Router: executeCommandAdapterRouter{service: delegate}, Router: executeCommandAdapterRouter{service: delegate},
@@ -342,7 +342,7 @@ func newAuthenticatedGRPCConfigForTest(mutate func(*config.AuthenticatedGRPCConf
return cfg 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 := newValidExecuteCommandRequestWithSessionAndRequestID(deviceSessionID, requestID)
req.MessageType = messageType req.MessageType = messageType
req.Signature = signRequest( req.Signature = signRequest(
+5 -5
View File
@@ -24,8 +24,8 @@ import (
"galaxy/gateway/internal/replay" "galaxy/gateway/internal/replay"
"galaxy/gateway/internal/session" "galaxy/gateway/internal/session"
"galaxy/gateway/internal/telemetry" "galaxy/gateway/internal/telemetry"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"galaxy/gateway/proto/galaxy/gateway/v1/gatewayv1connect" "galaxy/gateway/proto/edge/v1/edgev1connect"
"connectrpc.com/connect" "connectrpc.com/connect"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "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 // after the initial authenticated service event has been sent. When nil, the
// gateway keeps authenticated SubscribeEvents streams open until the client // gateway keeps authenticated SubscribeEvents streams open until the client
// cancels them, the server shuts down, or a later stream send fails. // 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 // Router resolves the exact downstream unary client for the verified
// message_type value. When nil, the authenticated unary surface uses an // message_type value. When nil, the authenticated unary surface uses an
@@ -93,7 +93,7 @@ type ServerDependencies struct {
// single net/http listener. // single net/http listener.
type Server struct { type Server struct {
cfg config.AuthenticatedGRPCConfig cfg config.AuthenticatedGRPCConfig
service gatewayv1.EdgeGatewayServer service edgev1.GatewayServer
logger *zap.Logger logger *zap.Logger
pushHub *push.Hub pushHub *push.Hub
metrics *telemetry.Runtime metrics *telemetry.Runtime
@@ -169,7 +169,7 @@ func (s *Server) Run(ctx context.Context) error {
mux := http.NewServeMux() mux := http.NewServeMux()
connectHandler := newConnectEdgeAdapter(s.service) connectHandler := newConnectEdgeAdapter(s.service)
path, handler := gatewayv1connect.NewEdgeGatewayHandler( path, handler := edgev1connect.NewGatewayHandler(
connectHandler, connectHandler,
connect.WithInterceptors(observabilityConnectInterceptor(s.logger, s.metrics)), connect.WithInterceptors(observabilityConnectInterceptor(s.logger, s.metrics)),
) )
+7 -7
View File
@@ -12,8 +12,8 @@ import (
"galaxy/gateway/internal/app" "galaxy/gateway/internal/app"
"galaxy/gateway/internal/config" "galaxy/gateway/internal/config"
"galaxy/gateway/internal/session" "galaxy/gateway/internal/session"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"galaxy/gateway/proto/galaxy/gateway/v1/gatewayv1connect" "galaxy/gateway/proto/edge/v1/edgev1connect"
"connectrpc.com/connect" "connectrpc.com/connect"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -30,7 +30,7 @@ func TestExecuteCommandRejectsMalformedEnvelope(t *testing.T) {
addr := waitForListenAddr(t, server) addr := waitForListenAddr(t, server)
client := newEdgeClient(t, addr) 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) require.Error(t, err)
assert.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) assert.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
} }
@@ -44,7 +44,7 @@ func TestSubscribeEventsRejectsMalformedEnvelope(t *testing.T) {
addr := waitForListenAddr(t, server) addr := waitForListenAddr(t, server)
client := newEdgeClient(t, addr) client := newEdgeClient(t, addr)
err := subscribeEventsError(t, context.Background(), client, &gatewayv1.SubscribeEventsRequest{}) err := subscribeEventsError(t, context.Background(), client, &edgev1.SubscribeEventsRequest{})
require.Error(t, err) require.Error(t, err)
assert.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) assert.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
} }
@@ -58,7 +58,7 @@ func TestExecuteCommandRejectsUnsupportedProtocolVersion(t *testing.T) {
addr := waitForListenAddr(t, server) addr := waitForListenAddr(t, server)
client := newEdgeClient(t, addr) client := newEdgeClient(t, addr)
_, err := client.ExecuteCommand(context.Background(), connect.NewRequest(&gatewayv1.ExecuteCommandRequest{ _, err := client.ExecuteCommand(context.Background(), connect.NewRequest(&edgev1.ExecuteCommandRequest{
ProtocolVersion: "v2", ProtocolVersion: "v2",
DeviceSessionId: "device-session-123", DeviceSessionId: "device-session-123",
MessageType: "fleet.move", 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 // authenticated edge listener. AllowHTTP forces the client to issue plain
// HTTP/2 requests (h2c) instead of attempting TLS, which the gateway's // HTTP/2 requests (h2c) instead of attempting TLS, which the gateway's
// in-process test bootstrap does not configure. // 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() t.Helper()
httpClient := &http.Client{ 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 // connectErrorMessage extracts the *connect.Error message from err. It
+8 -8
View File
@@ -5,7 +5,7 @@ import (
"errors" "errors"
"galaxy/gateway/internal/session" "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"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -30,15 +30,15 @@ func resolvedSessionFromContext(ctx context.Context) (session.Record, bool) {
// sessionLookupService resolves the authenticated session from SessionCache // sessionLookupService resolves the authenticated session from SessionCache
// after envelope parsing succeeds and before later auth steps run. // after envelope parsing succeeds and before later auth steps run.
type sessionLookupService struct { type sessionLookupService struct {
gatewayv1.UnimplementedEdgeGatewayServer edgev1.UnimplementedGatewayServer
delegate gatewayv1.EdgeGatewayServer delegate edgev1.GatewayServer
cache session.Cache cache session.Cache
} }
// ExecuteCommand resolves the cached session for req and only then forwards it // ExecuteCommand resolves the cached session for req and only then forwards it
// to the configured delegate with the resolved session attached to ctx. // 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) record, err := s.lookupSession(ctx)
if err != nil { if err != nil {
return nil, err 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 // SubscribeEvents resolves the cached session for req and only then forwards it
// to the configured delegate with the resolved session attached to the stream // to the configured delegate with the resolved session attached to the stream
// context. // 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()) record, err := s.lookupSession(stream.Context())
if err != nil { if err != nil {
return err return err
@@ -63,7 +63,7 @@ func (s sessionLookupService) SubscribeEvents(req *gatewayv1.SubscribeEventsRequ
} }
// newSessionLookupService wraps delegate with the session-cache lookup gate. // 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{ return sessionLookupService{
delegate: delegate, delegate: delegate,
cache: cache, cache: cache,
@@ -105,7 +105,7 @@ func cloneSessionRecord(record session.Record) session.Record {
type resolvedSessionContextKey struct{} type resolvedSessionContextKey struct{}
type resolvedSessionContextStream struct { type resolvedSessionContextStream struct {
grpc.ServerStreamingServer[gatewayv1.GatewayEvent] grpc.ServerStreamingServer[edgev1.GatewayEvent]
ctx context.Context ctx context.Context
} }
@@ -126,4 +126,4 @@ func (unavailableSessionCache) Lookup(context.Context, string) (session.Record,
func (unavailableSessionCache) MarkRevoked(string) {} func (unavailableSessionCache) MarkRevoked(string) {}
func (unavailableSessionCache) MarkAllRevokedForUser(string) {} func (unavailableSessionCache) MarkAllRevokedForUser(string) {}
var _ gatewayv1.EdgeGatewayServer = sessionLookupService{} var _ edgev1.GatewayServer = sessionLookupService{}
@@ -6,7 +6,7 @@ import (
"testing" "testing"
"galaxy/gateway/internal/session" "galaxy/gateway/internal/session"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"connectrpc.com/connect" "connectrpc.com/connect"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -17,7 +17,7 @@ import (
func TestExecuteCommandRejectsUnknownSession(t *testing.T) { func TestExecuteCommandRejectsUnknownSession(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{ SessionCache: staticSessionCache{
@@ -40,7 +40,7 @@ func TestExecuteCommandRejectsUnknownSession(t *testing.T) {
func TestSubscribeEventsRejectsUnknownSession(t *testing.T) { func TestSubscribeEventsRejectsUnknownSession(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{ SessionCache: staticSessionCache{
@@ -63,7 +63,7 @@ func TestSubscribeEventsRejectsUnknownSession(t *testing.T) {
func TestExecuteCommandRejectsRevokedSession(t *testing.T) { func TestExecuteCommandRejectsRevokedSession(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newRevokedSessionRecord(), nil }}, 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) { func TestSubscribeEventsRejectsRevokedSession(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newRevokedSessionRecord(), nil }}, 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) { func TestExecuteCommandRejectsSessionCacheUnavailable(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{ SessionCache: staticSessionCache{
@@ -124,7 +124,7 @@ func TestExecuteCommandRejectsSessionCacheUnavailable(t *testing.T) {
func TestSubscribeEventsRejectsSessionCacheUnavailable(t *testing.T) { func TestSubscribeEventsRejectsSessionCacheUnavailable(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{ SessionCache: staticSessionCache{
@@ -147,12 +147,12 @@ func TestSubscribeEventsRejectsSessionCacheUnavailable(t *testing.T) {
func TestExecuteCommandAttachesResolvedSession(t *testing.T) { func TestExecuteCommandAttachesResolvedSession(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{ delegate := &recordingGatewayService{
executeCommandFunc: func(ctx context.Context, req *gatewayv1.ExecuteCommandRequest) (*gatewayv1.ExecuteCommandResponse, error) { executeCommandFunc: func(ctx context.Context, req *edgev1.ExecuteCommandRequest) (*edgev1.ExecuteCommandResponse, error) {
record, ok := resolvedSessionFromContext(ctx) record, ok := resolvedSessionFromContext(ctx)
require.True(t, ok) require.True(t, ok)
assert.Equal(t, newActiveSessionRecord(), record) 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) { func TestSubscribeEventsAttachesResolvedSession(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{ delegate := &recordingGatewayService{
subscribeEventsFunc: func(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { subscribeEventsFunc: func(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error {
record, ok := resolvedSessionFromContext(stream.Context()) record, ok := resolvedSessionFromContext(stream.Context())
require.True(t, ok) require.True(t, ok)
assert.Equal(t, newActiveSessionRecord(), record) assert.Equal(t, newActiveSessionRecord(), record)
@@ -204,8 +204,8 @@ func TestSubscribeEventsAttachesResolvedSession(t *testing.T) {
func TestSubscribeEventsAttachesAuthenticatedStreamBinding(t *testing.T) { func TestSubscribeEventsAttachesAuthenticatedStreamBinding(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{ delegate := &recordingGatewayService{
subscribeEventsFunc: func(req *gatewayv1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[gatewayv1.GatewayEvent]) error { subscribeEventsFunc: func(req *edgev1.SubscribeEventsRequest, stream grpc.ServerStreamingServer[edgev1.GatewayEvent]) error {
binding, ok := authenticatedStreamBindingFromContext(stream.Context()) binding, ok := authenticatedStreamBindingFromContext(stream.Context())
require.True(t, ok) require.True(t, ok)
assert.Equal(t, authenticatedStreamBinding{ assert.Equal(t, authenticatedStreamBinding{
+7 -7
View File
@@ -5,7 +5,7 @@ import (
"errors" "errors"
"galaxy/gateway/authn" "galaxy/gateway/authn"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -15,14 +15,14 @@ import (
// signatureVerifyingService applies client-signature verification after // signatureVerifyingService applies client-signature verification after
// payload integrity checks and before later auth or routing steps run. // payload integrity checks and before later auth or routing steps run.
type signatureVerifyingService struct { type signatureVerifyingService struct {
gatewayv1.UnimplementedEdgeGatewayServer edgev1.UnimplementedGatewayServer
delegate gatewayv1.EdgeGatewayServer delegate edgev1.GatewayServer
} }
// ExecuteCommand verifies req client signature before delegating to the // ExecuteCommand verifies req client signature before delegating to the
// configured service implementation. // 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 { if err := verifyRequestSignature(ctx); err != nil {
return nil, err 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 // SubscribeEvents verifies req client signature before delegating to the
// configured service implementation. // 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 { if err := verifyRequestSignature(stream.Context()); err != nil {
return err return err
} }
@@ -42,7 +42,7 @@ func (s signatureVerifyingService) SubscribeEvents(req *gatewayv1.SubscribeEvent
// newSignatureVerifyingService wraps delegate with the client-signature // newSignatureVerifyingService wraps delegate with the client-signature
// verification gate. // verification gate.
func newSignatureVerifyingService(delegate gatewayv1.EdgeGatewayServer) gatewayv1.EdgeGatewayServer { func newSignatureVerifyingService(delegate edgev1.GatewayServer) edgev1.GatewayServer {
return signatureVerifyingService{delegate: delegate} return signatureVerifyingService{delegate: delegate}
} }
@@ -77,4 +77,4 @@ func verifyRequestSignature(ctx context.Context) error {
} }
} }
var _ gatewayv1.EdgeGatewayServer = signatureVerifyingService{} var _ edgev1.GatewayServer = signatureVerifyingService{}
@@ -14,7 +14,7 @@ import (
func TestExecuteCommandRejectsInvalidSignature(t *testing.T) { func TestExecuteCommandRejectsInvalidSignature(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, 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) { func TestExecuteCommandRejectsWrongKey(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{ SessionCache: staticSessionCache{
@@ -62,7 +62,7 @@ func TestExecuteCommandRejectsWrongKey(t *testing.T) {
func TestExecuteCommandRejectsInvalidCachedPublicKey(t *testing.T) { func TestExecuteCommandRejectsInvalidCachedPublicKey(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{ SessionCache: staticSessionCache{
@@ -87,7 +87,7 @@ func TestExecuteCommandRejectsInvalidCachedPublicKey(t *testing.T) {
func TestSubscribeEventsRejectsInvalidSignature(t *testing.T) { func TestSubscribeEventsRejectsInvalidSignature(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{lookupFunc: func(context.Context, string) (session.Record, error) { return newActiveSessionRecord(), nil }}, 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) { func TestSubscribeEventsRejectsWrongKey(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{ SessionCache: staticSessionCache{
@@ -135,7 +135,7 @@ func TestSubscribeEventsRejectsWrongKey(t *testing.T) {
func TestSubscribeEventsRejectsInvalidCachedPublicKey(t *testing.T) { func TestSubscribeEventsRejectsInvalidCachedPublicKey(t *testing.T) {
t.Parallel() t.Parallel()
delegate := &recordingEdgeGatewayService{} delegate := &recordingGatewayService{}
server, runGateway := newTestGateway(t, ServerDependencies{ server, runGateway := newTestGateway(t, ServerDependencies{
Service: delegate, Service: delegate,
SessionCache: staticSessionCache{ SessionCache: staticSessionCache{
+16 -16
View File
@@ -13,8 +13,8 @@ import (
"galaxy/gateway/authn" "galaxy/gateway/authn"
"galaxy/gateway/internal/downstream" "galaxy/gateway/internal/downstream"
"galaxy/gateway/internal/session" "galaxy/gateway/internal/session"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"galaxy/gateway/proto/galaxy/gateway/v1/gatewayv1connect" "galaxy/gateway/proto/edge/v1/edgev1connect"
gatewayfbs "galaxy/schema/fbs/gateway" gatewayfbs "galaxy/schema/fbs/gateway"
@@ -29,19 +29,19 @@ var (
testFreshnessWindow = 5 * time.Minute testFreshnessWindow = 5 * time.Minute
) )
func newValidExecuteCommandRequest() *gatewayv1.ExecuteCommandRequest { func newValidExecuteCommandRequest() *edgev1.ExecuteCommandRequest {
return newValidExecuteCommandRequestWithSessionAndRequestID("device-session-123", "request-123") 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()) 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") payloadBytes := []byte("payload")
payloadHash := sha256.Sum256(payloadBytes) payloadHash := sha256.Sum256(payloadBytes)
req := &gatewayv1.ExecuteCommandRequest{ req := &edgev1.ExecuteCommandRequest{
ProtocolVersion: supportedProtocolVersion, ProtocolVersion: supportedProtocolVersion,
DeviceSessionId: deviceSessionID, DeviceSessionId: deviceSessionID,
MessageType: "fleet.move", MessageType: "fleet.move",
@@ -56,18 +56,18 @@ func newValidExecuteCommandRequestWithTimestamp(deviceSessionID string, requestI
return req return req
} }
func newValidSubscribeEventsRequest() *gatewayv1.SubscribeEventsRequest { func newValidSubscribeEventsRequest() *edgev1.SubscribeEventsRequest {
return newValidSubscribeEventsRequestWithSessionAndRequestID("device-session-123", "request-123") 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()) 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) payloadHash := sha256.Sum256(nil)
req := &gatewayv1.SubscribeEventsRequest{ req := &edgev1.SubscribeEventsRequest{
ProtocolVersion: supportedProtocolVersion, ProtocolVersion: supportedProtocolVersion,
DeviceSessionId: deviceSessionID, DeviceSessionId: deviceSessionID,
MessageType: "gateway.subscribe", MessageType: "gateway.subscribe",
@@ -172,7 +172,7 @@ func (c fixedClock) Now() time.Time {
func recvBootstrapEvent(t interface { func recvBootstrapEvent(t interface {
require.TestingT require.TestingT
Helper() Helper()
}, stream *connect.ServerStreamForClient[gatewayv1.GatewayEvent]) *gatewayv1.GatewayEvent { }, stream *connect.ServerStreamForClient[edgev1.GatewayEvent]) *edgev1.GatewayEvent {
t.Helper() t.Helper()
if !stream.Receive() { if !stream.Receive() {
@@ -189,7 +189,7 @@ func recvBootstrapEvent(t interface {
func subscribeEventsError(t interface { func subscribeEventsError(t interface {
require.TestingT require.TestingT
Helper() Helper()
}, ctx context.Context, client gatewayv1connect.EdgeGatewayClient, req *gatewayv1.SubscribeEventsRequest) error { }, ctx context.Context, client edgev1connect.GatewayClient, req *edgev1.SubscribeEventsRequest) error {
t.Helper() t.Helper()
stream, err := client.SubscribeEvents(ctx, connect.NewRequest(req)) stream, err := client.SubscribeEvents(ctx, connect.NewRequest(req))
@@ -208,7 +208,7 @@ func subscribeEventsError(t interface {
func assertServerTimeBootstrapEvent(t interface { func assertServerTimeBootstrapEvent(t interface {
require.TestingT require.TestingT
Helper() 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() t.Helper()
require.NotNil(t, event) require.NotNil(t, event)
@@ -244,7 +244,7 @@ func (s staticReplayStore) Reserve(ctx context.Context, deviceSessionID string,
} }
type executeCommandAdapterRouter struct { type executeCommandAdapterRouter struct {
service gatewayv1.EdgeGatewayServer service edgev1.GatewayServer
} }
func (r executeCommandAdapterRouter) Route(string) (downstream.Client, error) { func (r executeCommandAdapterRouter) Route(string) (downstream.Client, error) {
@@ -252,11 +252,11 @@ func (r executeCommandAdapterRouter) Route(string) (downstream.Client, error) {
} }
type executeCommandAdapterClient struct { type executeCommandAdapterClient struct {
service gatewayv1.EdgeGatewayServer service edgev1.GatewayServer
} }
func (c executeCommandAdapterClient) ExecuteCommand(ctx context.Context, command downstream.AuthenticatedCommand) (downstream.UnaryResult, error) { 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, ProtocolVersion: command.ProtocolVersion,
DeviceSessionId: command.DeviceSessionID, DeviceSessionId: command.DeviceSessionID,
MessageType: command.MessageType, MessageType: command.MessageType,
+8
View File
@@ -6,6 +6,14 @@ info:
This specification documents the implemented `galaxy/gateway` v1 public This specification documents the implemented `galaxy/gateway` v1 public
REST surface. 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://<host>/healthz` and
`https://<host>/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: Implemented endpoints:
- `GET /healthz` - `GET /healthz`
- `GET /readyz` - `GET /readyz`
@@ -2,9 +2,9 @@
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.11
// protoc (unknown) // protoc (unknown)
// source: galaxy/gateway/v1/edge_gateway.proto // source: edge/v1/edge_gateway.proto
package gatewayv1 package edgev1
import ( import (
_ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate"
@@ -42,7 +42,7 @@ type ExecuteCommandRequest struct {
func (x *ExecuteCommandRequest) Reset() { func (x *ExecuteCommandRequest) Reset() {
*x = ExecuteCommandRequest{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -54,7 +54,7 @@ func (x *ExecuteCommandRequest) String() string {
func (*ExecuteCommandRequest) ProtoMessage() {} func (*ExecuteCommandRequest) ProtoMessage() {}
func (x *ExecuteCommandRequest) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -67,7 +67,7 @@ func (x *ExecuteCommandRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExecuteCommandRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ExecuteCommandRequest.ProtoReflect.Descriptor instead.
func (*ExecuteCommandRequest) Descriptor() ([]byte, []int) { 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 { func (x *ExecuteCommandRequest) GetProtocolVersion() string {
@@ -148,7 +148,7 @@ type ExecuteCommandResponse struct {
func (x *ExecuteCommandResponse) Reset() { func (x *ExecuteCommandResponse) Reset() {
*x = ExecuteCommandResponse{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -160,7 +160,7 @@ func (x *ExecuteCommandResponse) String() string {
func (*ExecuteCommandResponse) ProtoMessage() {} func (*ExecuteCommandResponse) ProtoMessage() {}
func (x *ExecuteCommandResponse) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -173,7 +173,7 @@ func (x *ExecuteCommandResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExecuteCommandResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ExecuteCommandResponse.ProtoReflect.Descriptor instead.
func (*ExecuteCommandResponse) Descriptor() ([]byte, []int) { 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 { func (x *ExecuteCommandResponse) GetProtocolVersion() string {
@@ -246,7 +246,7 @@ type SubscribeEventsRequest struct {
func (x *SubscribeEventsRequest) Reset() { func (x *SubscribeEventsRequest) Reset() {
*x = SubscribeEventsRequest{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -258,7 +258,7 @@ func (x *SubscribeEventsRequest) String() string {
func (*SubscribeEventsRequest) ProtoMessage() {} func (*SubscribeEventsRequest) ProtoMessage() {}
func (x *SubscribeEventsRequest) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -271,7 +271,7 @@ func (x *SubscribeEventsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SubscribeEventsRequest.ProtoReflect.Descriptor instead. // Deprecated: Use SubscribeEventsRequest.ProtoReflect.Descriptor instead.
func (*SubscribeEventsRequest) Descriptor() ([]byte, []int) { 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 { func (x *SubscribeEventsRequest) GetProtocolVersion() string {
@@ -353,7 +353,7 @@ type GatewayEvent struct {
func (x *GatewayEvent) Reset() { func (x *GatewayEvent) Reset() {
*x = GatewayEvent{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -365,7 +365,7 @@ func (x *GatewayEvent) String() string {
func (*GatewayEvent) ProtoMessage() {} func (*GatewayEvent) ProtoMessage() {}
func (x *GatewayEvent) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -378,7 +378,7 @@ func (x *GatewayEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use GatewayEvent.ProtoReflect.Descriptor instead. // Deprecated: Use GatewayEvent.ProtoReflect.Descriptor instead.
func (*GatewayEvent) Descriptor() ([]byte, []int) { 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 { func (x *GatewayEvent) GetEventType() string {
@@ -437,11 +437,11 @@ func (x *GatewayEvent) GetTraceId() string {
return "" 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" + "\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" + "\x15ExecuteCommandRequest\x122\n" +
"\x10protocol_version\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x0fprotocolVersion\x123\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" + "\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" + "\tsignature\x18\x06 \x01(\fR\tsignature\x12\x1d\n" +
"\n" + "\n" +
"request_id\x18\a \x01(\tR\trequestId\x12\x19\n" + "request_id\x18\a \x01(\tR\trequestId\x12\x19\n" +
"\btrace_id\x18\b \x01(\tR\atraceId2\xd5\x01\n" + "\btrace_id\x18\b \x01(\tR\atraceId2\xa9\x01\n" +
"\vEdgeGateway\x12e\n" + "\aGateway\x12Q\n" +
"\x0eExecuteCommand\x12(.galaxy.gateway.v1.ExecuteCommandRequest\x1a).galaxy.gateway.v1.ExecuteCommandResponse\x12_\n" + "\x0eExecuteCommand\x12\x1e.edge.v1.ExecuteCommandRequest\x1a\x1f.edge.v1.ExecuteCommandResponse\x12K\n" +
"\x0fSubscribeEvents\x12).galaxy.gateway.v1.SubscribeEventsRequest\x1a\x1f.galaxy.gateway.v1.GatewayEvent0\x01B2Z0galaxy/gateway/proto/galaxy/gateway/v1;gatewayv1b\x06proto3" "\x0fSubscribeEvents\x12\x1f.edge.v1.SubscribeEventsRequest\x1a\x15.edge.v1.GatewayEvent0\x01B%Z#galaxy/gateway/proto/edge/v1;edgev1b\x06proto3"
var ( var (
file_galaxy_gateway_v1_edge_gateway_proto_rawDescOnce sync.Once file_edge_v1_edge_gateway_proto_rawDescOnce sync.Once
file_galaxy_gateway_v1_edge_gateway_proto_rawDescData []byte file_edge_v1_edge_gateway_proto_rawDescData []byte
) )
func file_galaxy_gateway_v1_edge_gateway_proto_rawDescGZIP() []byte { func file_edge_v1_edge_gateway_proto_rawDescGZIP() []byte {
file_galaxy_gateway_v1_edge_gateway_proto_rawDescOnce.Do(func() { file_edge_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))) 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_edge_v1_edge_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_galaxy_gateway_v1_edge_gateway_proto_goTypes = []any{ var file_edge_v1_edge_gateway_proto_goTypes = []any{
(*ExecuteCommandRequest)(nil), // 0: galaxy.gateway.v1.ExecuteCommandRequest (*ExecuteCommandRequest)(nil), // 0: edge.v1.ExecuteCommandRequest
(*ExecuteCommandResponse)(nil), // 1: galaxy.gateway.v1.ExecuteCommandResponse (*ExecuteCommandResponse)(nil), // 1: edge.v1.ExecuteCommandResponse
(*SubscribeEventsRequest)(nil), // 2: galaxy.gateway.v1.SubscribeEventsRequest (*SubscribeEventsRequest)(nil), // 2: edge.v1.SubscribeEventsRequest
(*GatewayEvent)(nil), // 3: galaxy.gateway.v1.GatewayEvent (*GatewayEvent)(nil), // 3: edge.v1.GatewayEvent
} }
var file_galaxy_gateway_v1_edge_gateway_proto_depIdxs = []int32{ var file_edge_v1_edge_gateway_proto_depIdxs = []int32{
0, // 0: galaxy.gateway.v1.EdgeGateway.ExecuteCommand:input_type -> galaxy.gateway.v1.ExecuteCommandRequest 0, // 0: edge.v1.Gateway.ExecuteCommand:input_type -> edge.v1.ExecuteCommandRequest
2, // 1: galaxy.gateway.v1.EdgeGateway.SubscribeEvents:input_type -> galaxy.gateway.v1.SubscribeEventsRequest 2, // 1: edge.v1.Gateway.SubscribeEvents:input_type -> edge.v1.SubscribeEventsRequest
1, // 2: galaxy.gateway.v1.EdgeGateway.ExecuteCommand:output_type -> galaxy.gateway.v1.ExecuteCommandResponse 1, // 2: edge.v1.Gateway.ExecuteCommand:output_type -> edge.v1.ExecuteCommandResponse
3, // 3: galaxy.gateway.v1.EdgeGateway.SubscribeEvents:output_type -> galaxy.gateway.v1.GatewayEvent 3, // 3: edge.v1.Gateway.SubscribeEvents:output_type -> edge.v1.GatewayEvent
2, // [2:4] is the sub-list for method output_type 2, // [2:4] is the sub-list for method output_type
0, // [0:2] is the sub-list for method input_type 0, // [0:2] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name 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 0, // [0:0] is the sub-list for field type_name
} }
func init() { file_galaxy_gateway_v1_edge_gateway_proto_init() } func init() { file_edge_v1_edge_gateway_proto_init() }
func file_galaxy_gateway_v1_edge_gateway_proto_init() { func file_edge_v1_edge_gateway_proto_init() {
if File_galaxy_gateway_v1_edge_gateway_proto != nil { if File_edge_v1_edge_gateway_proto != nil {
return return
} }
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 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, NumEnums: 0,
NumMessages: 4, NumMessages: 4,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
GoTypes: file_galaxy_gateway_v1_edge_gateway_proto_goTypes, GoTypes: file_edge_v1_edge_gateway_proto_goTypes,
DependencyIndexes: file_galaxy_gateway_v1_edge_gateway_proto_depIdxs, DependencyIndexes: file_edge_v1_edge_gateway_proto_depIdxs,
MessageInfos: file_galaxy_gateway_v1_edge_gateway_proto_msgTypes, MessageInfos: file_edge_v1_edge_gateway_proto_msgTypes,
}.Build() }.Build()
File_galaxy_gateway_v1_edge_gateway_proto = out.File File_edge_v1_edge_gateway_proto = out.File
file_galaxy_gateway_v1_edge_gateway_proto_goTypes = nil file_edge_v1_edge_gateway_proto_goTypes = nil
file_galaxy_gateway_v1_edge_gateway_proto_depIdxs = nil file_edge_v1_edge_gateway_proto_depIdxs = nil
} }
@@ -1,12 +1,12 @@
syntax = "proto3"; 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"; import "buf/validate/validate.proto";
service EdgeGateway { service Gateway {
rpc ExecuteCommand(ExecuteCommandRequest) returns (ExecuteCommandResponse); rpc ExecuteCommand(ExecuteCommandRequest) returns (ExecuteCommandResponse);
rpc SubscribeEvents(SubscribeEventsRequest) returns (stream GatewayEvent); rpc SubscribeEvents(SubscribeEventsRequest) returns (stream GatewayEvent);
} }
@@ -2,9 +2,9 @@
// versions: // versions:
// - protoc-gen-go-grpc v1.6.1 // - protoc-gen-go-grpc v1.6.1
// - protoc (unknown) // - protoc (unknown)
// source: galaxy/gateway/v1/edge_gateway.proto // source: edge/v1/edge_gateway.proto
package gatewayv1 package edgev1
import ( import (
context "context" context "context"
@@ -19,39 +19,39 @@ import (
const _ = grpc.SupportPackageIsVersion9 const _ = grpc.SupportPackageIsVersion9
const ( const (
EdgeGateway_ExecuteCommand_FullMethodName = "/galaxy.gateway.v1.EdgeGateway/ExecuteCommand" Gateway_ExecuteCommand_FullMethodName = "/edge.v1.Gateway/ExecuteCommand"
EdgeGateway_SubscribeEvents_FullMethodName = "/galaxy.gateway.v1.EdgeGateway/SubscribeEvents" 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. // 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) ExecuteCommand(ctx context.Context, in *ExecuteCommandRequest, opts ...grpc.CallOption) (*ExecuteCommandResponse, error)
SubscribeEvents(ctx context.Context, in *SubscribeEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GatewayEvent], error) SubscribeEvents(ctx context.Context, in *SubscribeEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GatewayEvent], error)
} }
type edgeGatewayClient struct { type gatewayClient struct {
cc grpc.ClientConnInterface cc grpc.ClientConnInterface
} }
func NewEdgeGatewayClient(cc grpc.ClientConnInterface) EdgeGatewayClient { func NewGatewayClient(cc grpc.ClientConnInterface) GatewayClient {
return &edgeGatewayClient{cc} 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...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ExecuteCommandResponse) 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 { if err != nil {
return nil, err return nil, err
} }
return out, nil 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...) 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 { if err != nil {
return nil, err 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. // 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. // GatewayServer is the server API for Gateway service.
// All implementations must embed UnimplementedEdgeGatewayServer // All implementations must embed UnimplementedGatewayServer
// for forward compatibility. // for forward compatibility.
type EdgeGatewayServer interface { type GatewayServer interface {
ExecuteCommand(context.Context, *ExecuteCommandRequest) (*ExecuteCommandResponse, error) ExecuteCommand(context.Context, *ExecuteCommandRequest) (*ExecuteCommandResponse, error)
SubscribeEvents(*SubscribeEventsRequest, grpc.ServerStreamingServer[GatewayEvent]) error SubscribeEvents(*SubscribeEventsRequest, grpc.ServerStreamingServer[GatewayEvent]) error
mustEmbedUnimplementedEdgeGatewayServer() mustEmbedUnimplementedGatewayServer()
} }
// UnimplementedEdgeGatewayServer must be embedded to have // UnimplementedGatewayServer must be embedded to have
// forward compatible implementations. // forward compatible implementations.
// //
// NOTE: this should be embedded by value instead of pointer to avoid a nil // NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called. // 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") 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") return status.Error(codes.Unimplemented, "method SubscribeEvents not implemented")
} }
func (UnimplementedEdgeGatewayServer) mustEmbedUnimplementedEdgeGatewayServer() {} func (UnimplementedGatewayServer) mustEmbedUnimplementedGatewayServer() {}
func (UnimplementedEdgeGatewayServer) testEmbeddedByValue() {} func (UnimplementedGatewayServer) testEmbeddedByValue() {}
// UnsafeEdgeGatewayServer may be embedded to opt out of forward compatibility for this service. // UnsafeGatewayServer 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 // Use of this interface is not recommended, as added methods to GatewayServer will
// result in compilation errors. // result in compilation errors.
type UnsafeEdgeGatewayServer interface { type UnsafeGatewayServer interface {
mustEmbedUnimplementedEdgeGatewayServer() mustEmbedUnimplementedGatewayServer()
} }
func RegisterEdgeGatewayServer(s grpc.ServiceRegistrar, srv EdgeGatewayServer) { func RegisterGatewayServer(s grpc.ServiceRegistrar, srv GatewayServer) {
// If the following call panics, it indicates UnimplementedEdgeGatewayServer was // If the following call panics, it indicates UnimplementedGatewayServer was
// embedded by pointer and is nil. This will cause panics if an // embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization // unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O. // time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue() 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) in := new(ExecuteCommandRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(EdgeGatewayServer).ExecuteCommand(ctx, in) return srv.(GatewayServer).ExecuteCommand(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: EdgeGateway_ExecuteCommand_FullMethodName, FullMethod: Gateway_ExecuteCommand_FullMethodName,
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { 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) 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) m := new(SubscribeEventsRequest)
if err := stream.RecvMsg(m); err != nil { if err := stream.RecvMsg(m); err != nil {
return err 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. // 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, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
var EdgeGateway_ServiceDesc = grpc.ServiceDesc{ var Gateway_ServiceDesc = grpc.ServiceDesc{
ServiceName: "galaxy.gateway.v1.EdgeGateway", ServiceName: "edge.v1.Gateway",
HandlerType: (*EdgeGatewayServer)(nil), HandlerType: (*GatewayServer)(nil),
Methods: []grpc.MethodDesc{ Methods: []grpc.MethodDesc{
{ {
MethodName: "ExecuteCommand", MethodName: "ExecuteCommand",
Handler: _EdgeGateway_ExecuteCommand_Handler, Handler: _Gateway_ExecuteCommand_Handler,
}, },
}, },
Streams: []grpc.StreamDesc{ Streams: []grpc.StreamDesc{
{ {
StreamName: "SubscribeEvents", StreamName: "SubscribeEvents",
Handler: _EdgeGateway_SubscribeEvents_Handler, Handler: _Gateway_SubscribeEvents_Handler,
ServerStreams: true, ServerStreams: true,
}, },
}, },
Metadata: "galaxy/gateway/v1/edge_gateway.proto", Metadata: "edge/v1/edge_gateway.proto",
} }
@@ -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"))
}
@@ -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"))
}
+8 -8
View File
@@ -15,8 +15,8 @@ import (
"time" "time"
gatewayauthn "galaxy/gateway/authn" gatewayauthn "galaxy/gateway/authn"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1" edgev1 "galaxy/gateway/proto/edge/v1"
"galaxy/gateway/proto/galaxy/gateway/v1/gatewayv1connect" "galaxy/gateway/proto/edge/v1/edgev1connect"
"connectrpc.com/connect" "connectrpc.com/connect"
"github.com/google/uuid" "github.com/google/uuid"
@@ -32,7 +32,7 @@ import (
// alongside gRPC and gRPC-Web on the same port. // alongside gRPC and gRPC-Web on the same port.
type SignedGatewayClient struct { type SignedGatewayClient struct {
httpClient *http.Client httpClient *http.Client
edge gatewayv1connect.EdgeGatewayClient edge edgev1connect.GatewayClient
deviceSID string deviceSID string
privateKey ed25519.PrivateKey privateKey ed25519.PrivateKey
respPub ed25519.PublicKey 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{ return &SignedGatewayClient{
httpClient: httpClient, httpClient: httpClient,
@@ -164,7 +164,7 @@ func (c *SignedGatewayClient) Execute(ctx context.Context, messageType string, p
signature = ed25519.Sign(c.privateKey, input) signature = ed25519.Sign(c.privateKey, input)
} }
req := &gatewayv1.ExecuteCommandRequest{ req := &edgev1.ExecuteCommandRequest{
ProtocolVersion: protocolVersion, ProtocolVersion: protocolVersion,
DeviceSessionId: deviceSID, DeviceSessionId: deviceSID,
MessageType: messageType, 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 // authenticated event the gateway delivers; the channel closes when
// the stream ends or when ctx is done. Errors land on the err // the stream ends or when ctx is done. Errors land on the err
// channel. // 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() requestID := uuid.NewString()
timestampMS := time.Now().UnixMilli() timestampMS := time.Now().UnixMilli()
protocolVersion := "v1" protocolVersion := "v1"
@@ -224,7 +224,7 @@ func (c *SignedGatewayClient) SubscribeEvents(ctx context.Context, messageType s
PayloadHash: emptyHash[:], PayloadHash: emptyHash[:],
})) }))
stream, err := c.edge.SubscribeEvents(ctx, connect.NewRequest(&gatewayv1.SubscribeEventsRequest{ stream, err := c.edge.SubscribeEvents(ctx, connect.NewRequest(&edgev1.SubscribeEventsRequest{
ProtocolVersion: protocolVersion, ProtocolVersion: protocolVersion,
DeviceSessionId: c.deviceSID, DeviceSessionId: c.deviceSID,
MessageType: messageType, 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) 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) errs := make(chan error, 1)
go func() { go func() {
defer close(events) defer close(events)
+3
View File
@@ -0,0 +1,3 @@
node_modules/
.vitepress/dist/
.vitepress/cache/
+55
View File
@@ -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/" }],
},
],
},
},
},
});
+10
View File
@@ -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);
}
+6
View File
@@ -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;
+36
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
# Galaxy
A turn-based space strategy game.
[Play the game →](/game/)
+16
View File
@@ -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"
}
}
+1736
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
allowBuilds:
esbuild: true
+5
View File
@@ -0,0 +1,5 @@
# Galaxy
Пошаговая космическая стратегия.
[Играть →](/game/)
+58 -41
View File
@@ -1,51 +1,68 @@
# Application-routing Caddy for the long-lived dev environment. # Application-routing Caddy for the long-lived dev environment.
# Listens only on the `edge` Docker network; TLS termination and the # Single-origin, path-based: the project site, the game UI, and both
# real `:80`/`:443` listeners belong to the host Caddy in front of us. # 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, # / -> project site (galaxy-dev-site-dist -> /srv/galaxy-site)
# refreshed on every dev-deploy run. # /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
} }
:80 { :80 {
handle @frontend { # Authenticated Connect-Web edge. The browser calls
root * /srv/galaxy-ui # `/rpc/edge.v1.Gateway/<Method>`; 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
}
# `_app/immutable/`; the file name changes whenever the # Gateway public REST (auth) and the health probe on :8080.
# content changes, so the browser can cache them forever. @api path /api/* /healthz
# Without an explicit Cache-Control, Caddy falls back to handle @api {
# heuristic caching that revalidates on every reload — reverse_proxy galaxy-api:8080
# 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"
file_server # Bare `/game` (no trailing slash) -> `/game/` so the SPA root
encode zstd gzip # resolves before the site catch-all can claim it.
} handle /game {
redir * /game/ 308
}
handle @api { # Game UI under `/game/`. The bundle is built with base=/game, so it
# Connect-Web (authenticated) lives on a separate listener # references `/game/_app/...`; strip the prefix to serve the build
# (`GATEWAY_AUTHENTICATED_GRPC_ADDR=:9090`). Anything else — # whose files sit at the volume root. SPA fallback to index.html.
# public auth, healthz — is the public REST listener on handle_path /game/* {
# `:8080`. The split mirrors the Vite dev-server proxy in root * /srv/galaxy-ui
# `ui/frontend/vite.config.ts`. # Hash-named, content-addressed chunks: cache forever.
@connect path /galaxy.gateway.v1.EdgeGateway/* @immutable path /_app/immutable/*
handle @connect { header @immutable Cache-Control "public, max-age=31536000, immutable"
reverse_proxy galaxy-api:9090 # index.html, env.js, version.json, core.wasm, wasm_exec.js,
} # favicon, manifest, service-worker.js must revalidate so a
reverse_proxy galaxy-api:8080 # 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
}
} }
+40 -21
View File
@@ -1,25 +1,44 @@
# Production placeholder. Mirrors `Caddyfile.dev` but uses real # Production placeholder. Single-origin, path-based — mirrors
# hostnames and lets Caddy auto-provision TLS certificates. Not used # `Caddyfile.dev` but binds the real public host and lets Caddy
# until prod-deploy plumbing exists; kept under version control so the # auto-provision TLS. Not used until prod-deploy plumbing exists; kept
# dev/prod surface stays symmetric. # 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 { {$GALAXY_PUBLIC_HOST:galaxy.example} {
root * /srv/galaxy-ui handle_path /rpc/* {
reverse_proxy galaxy-api:9090
}
# Mirrors the cache policy `Caddyfile.dev` documents in detail: @api path /api/* /healthz
# SvelteKit's hash-named `_app/immutable/*` is safe to cache handle @api {
# forever; everything else must revalidate so a deploy reaches reverse_proxy galaxy-api:8080
# 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"
try_files {path} /index.html handle /game {
file_server redir * /game/ 308
encode zstd gzip }
}
handle_path /game/* {
api.galaxy.com { root * /srv/galaxy-ui
reverse_proxy galaxy-api:8080 @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
}
} }
+32 -8
View File
@@ -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 .DEFAULT_GOAL := help
@@ -6,6 +6,9 @@ REPO_ROOT := $(realpath $(CURDIR)/../..)
ENGINE_IMAGE := galaxy-engine:dev ENGINE_IMAGE := galaxy-engine:dev
STACK_LABEL := galaxy.stack=dev-deploy STACK_LABEL := galaxy.stack=dev-deploy
ENGINE_LABEL := org.opencontainers.image.title=galaxy-game-engine 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 # Game-state root lives under the invoking user's home by default so
# `make up` works without sudo. Override `GALAXY_DEV_GAME_STATE_DIR` # `make up` works without sudo. Override `GALAXY_DEV_GAME_STATE_DIR`
# in the environment or `.env` to relocate (e.g. /var/lib/galaxy-dev/ # 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 COMPOSE := docker compose
help: 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 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 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 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-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 seed-geoip Copy GeoIP fixture into galaxy-dev-geoip-data volume"
@echo " make down Stop containers, keep named volumes" @echo " make down Stop containers, keep named volumes"
@echo " make logs Tail all logs" @echo " make logs Tail all logs"
@@ -33,7 +37,7 @@ help:
@echo "Requires:" @echo "Requires:"
@echo " - external Docker network '$${GALAXY_EDGE_NETWORK:-edge}'" @echo " - external Docker network '$${GALAXY_EDGE_NETWORK:-edge}'"
@echo " (docker network create 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)" @echo " - game-state dir: $(GALAXY_DEV_GAME_STATE_DIR) (auto-created)"
up: build-engine seed-geoip up: build-engine seed-geoip
@@ -76,7 +80,8 @@ seed-ui:
fi fi
@echo "building UI (vite build)…" @echo "building UI (vite build)…"
(cd $(REPO_ROOT)/ui/frontend && \ (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_GALAXY_DEV_AFFORDANCES=true \
VITE_GATEWAY_RESPONSE_PUBLIC_KEY=$$(cat $(REPO_ROOT)/ui/frontend/.env.development \ VITE_GATEWAY_RESPONSE_PUBLIC_KEY=$$(cat $(REPO_ROOT)/ui/frontend/.env.development \
| sed -n 's/^VITE_GATEWAY_RESPONSE_PUBLIC_KEY=//p') \ | sed -n 's/^VITE_GATEWAY_RESPONSE_PUBLIC_KEY=//p') \
@@ -88,6 +93,23 @@ seed-ui:
-v $(REPO_ROOT)/ui/frontend/build:/src:ro \ -v $(REPO_ROOT)/ui/frontend/build:/src:ro \
alpine sh -c 'rm -rf /dst/* /dst/.??* 2>/dev/null; cp -a /src/. /dst/' 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: down:
$(COMPOSE) down $(COMPOSE) down
@@ -98,10 +120,12 @@ status:
$(COMPOSE) ps $(COMPOSE) ps
health: health:
@echo "Frontend (https://www.galaxy.lan):" @echo "Site (https://$(GALAXY_DEV_HOST)/):"
@curl -sS -o /dev/null -w " HTTP %{http_code}\n" https://www.galaxy.lan/ || echo " unreachable" @curl -sS -o /dev/null -w " HTTP %{http_code}\n" https://$(GALAXY_DEV_HOST)/ || echo " unreachable"
@echo "API healthz (https://api.galaxy.lan/healthz):" @echo "Game (https://$(GALAXY_DEV_HOST)/game/):"
@curl -sS -o /dev/null -w " HTTP %{http_code}\n" https://api.galaxy.lan/healthz || echo " unreachable" @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: psql:
$(COMPOSE) exec galaxy-postgres psql -U galaxy -d galaxy_backend $(COMPOSE) exec galaxy-postgres psql -U galaxy -d galaxy_backend
+57 -29
View File
@@ -2,11 +2,26 @@
A docker-compose stack that runs the Galaxy backend, gateway, supporting A docker-compose stack that runs the Galaxy backend, gateway, supporting
services, and a small Caddy in front of them, reachable through the host 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 Caddy at a single origin (`https://galaxy.lan` in dev). The stack is
the `dev-deploy.yaml` Gitea Actions workflow as the canonical dev target single-origin and path-based: the project site, the game UI, and both
on every merge into the `development` branch, and runnable by hand gateway surfaces live behind one host with no host name baked into the
through this Makefile for local debugging of the deploy plumbing artifacts. Used by the `dev-deploy.yaml` Gitea Actions workflow as the
itself. 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 — This stack is **not** the developer's primary playground for UI work —
that role still belongs to [`tools/local-dev/`](../local-dev/README.md), 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` - A host Caddy listening on `:80`/`:443`, attached to the `edge`
network, and proxying `www.galaxy.lan` and `api.galaxy.lan` to network, and proxying the single dev host `galaxy.lan` to
`galaxy-caddy:80`. Example fragment for the host Caddyfile: `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 ```caddy
www.galaxy.lan, api.galaxy.lan { galaxy.lan {
tls internal tls internal
reverse_proxy galaxy-caddy:80 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 `up` (re)builds the local-dev backend and gateway images, makes sure the
engine image `galaxy-engine:dev` exists, and waits for healthchecks. It 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 does **not** seed the UI or site volumes — that is normally done by CI.
time you run by hand: The first time you run by hand:
```sh ```sh
make -C tools/dev-deploy seed-site
make -C tools/dev-deploy seed-ui make -C tools/dev-deploy seed-ui
make -C tools/dev-deploy up make -C tools/dev-deploy up
make -C tools/dev-deploy health make -C tools/dev-deploy health
``` ```
`seed-ui` runs `pnpm build` in `ui/frontend/`, then copies the resulting `seed-ui` runs `pnpm build` in `ui/frontend/` (base path `/game`), then
`build/` tree into the `galaxy-dev-ui-dist` volume. Subsequent CI deploys copies the resulting `build/` tree into the `galaxy-dev-ui-dist` volume.
overwrite this volume automatically. `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 ## Daily flow
```sh ```sh
make -C tools/dev-deploy rebuild # rebuild backend/gateway images + up 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 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 make -C tools/dev-deploy down # stop, keep state
``` ```
@@ -109,14 +129,16 @@ cannot leak into the prod environment.
``` ```
Browser 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) 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) galaxy-caddy (networks: edge + galaxy-dev-internal)
www.galaxy.lan → file_server /srv/galaxy-ui (volume galaxy-dev-ui-dist) / -> file_server /srv/galaxy-site (volume galaxy-dev-site-dist)
api.galaxy.lan → reverse_proxy galaxy-api:8080 /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-dev-internal
├─ galaxy-api (gateway: :8080 REST, :9090 gRPC) ├─ galaxy-api (gateway: :8080 REST, :9090 gRPC)
@@ -155,13 +177,14 @@ The same volume-persistence model applies to `tools/local-dev/`.
```text ```text
make up Build images, ensure engine image, seed geoip, bring stack up make up Build images, ensure engine image, seed geoip, bring stack up
make rebuild Rebuild backend / gateway images (ignores cache), then 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 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 build-engine Build galaxy-engine:dev (no-op if image already present)
make down Stop containers, keep named volumes make down Stop containers, keep named volumes
make logs Tail compose logs make logs Tail compose logs
make status docker compose ps 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 psql psql as galaxy@galaxy_backend
make clean-data Stop everything and wipe volumes + game-state dir 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 ## Files
- `docker-compose.yml` — six services: postgres, redis, mailpit, - `docker-compose.yml` — six services: postgres, redis, mailpit,
galaxy-backend, galaxy-api, galaxy-caddy. Reuses the alpine-runtime galaxy-backend, galaxy-api, galaxy-caddy. `galaxy-caddy` mounts both
Dockerfiles from `../local-dev/` so the backend healthcheck can run the `galaxy-dev-site-dist` (`/srv/galaxy-site`) and
`wget`. Reuses the dev keypair from `../local-dev/keys/`. `galaxy-dev-ui-dist` (`/srv/galaxy-ui`) volumes and reverse-proxies
- `Caddyfile.dev` — the application-routing Caddy config, mounted into both gateway tiers (REST/health on `:8080`, Connect/gRPC-web on
`galaxy-caddy` at `/etc/caddy/Caddyfile`. `: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 - `Caddyfile.prod` — placeholder for a future prod deployment; not used
by this compose. by this compose.
- `Makefile` — wrapper over `docker compose` with helpers for engine, - `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:-}` - `.env.example` — non-secret defaults for the compose `${VAR:-}`
expansions. Copy to `.env` if you want host-local overrides. 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, - `tools/local-dev/` — single-developer playground, host-port mapped,
Vite dev server on the side. Recommended for active UI work. Vite dev server on the side. Recommended for active UI work.
- `.gitea/workflows/dev-deploy.yaml` — the CI side of this stack: - `.gitea/workflows/dev-deploy.yaml` — the CI side of this stack:
builds images, seeds the UI volume, runs `docker compose up -d` on builds images, seeds the site and UI volumes, runs `docker compose
every merge into `development`. The Makefile in this directory is up -d` on every merge into `development`. The Makefile in this
what that workflow ultimately calls into. directory is what that workflow ultimately calls into.
+11 -5
View File
@@ -186,11 +186,14 @@ services:
GATEWAY_RESPONSE_SIGNER_PRIVATE_KEY_PEM_PATH: /run/secrets/gateway-response.pem GATEWAY_RESPONSE_SIGNER_PRIVATE_KEY_PEM_PATH: /run/secrets/gateway-response.pem
GATEWAY_REDIS_MASTER_ADDR: "galaxy-redis:6379" GATEWAY_REDIS_MASTER_ADDR: "galaxy-redis:6379"
GATEWAY_REDIS_PASSWORD: galaxy-dev GATEWAY_REDIS_PASSWORD: galaxy-dev
# UI lives on https://www.galaxy.lan; the API is on # Single-origin deployment: the UI, public REST, and Connect-Web
# https://api.galaxy.lan. Browsers therefore issue cross-origin # edge share one host, so browser requests are same-origin and
# requests to the gateway and need an explicit allow-list. # CORS is not needed. An empty allow-list disables the CORS
GATEWAY_PUBLIC_HTTP_CORS_ALLOWED_ORIGINS: "https://www.galaxy.lan" # middleware (requests pass through without Access-Control-*
GATEWAY_AUTHENTICATED_GRPC_CORS_ALLOWED_ORIGINS: "https://www.galaxy.lan" # 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 # Anti-abuse defaults are looser than production: the dev
# environment is shared by a handful of trusted testers who # environment is shared by a handful of trusted testers who
# frequently hammer the same identity to reproduce flows. # frequently hammer the same identity to reproduce flows.
@@ -237,6 +240,7 @@ services:
- ./Caddyfile.dev:/etc/caddy/Caddyfile:ro - ./Caddyfile.dev:/etc/caddy/Caddyfile:ro
- galaxy-dev-caddy-data:/data - galaxy-dev-caddy-data:/data
- galaxy-dev-ui-dist:/srv/galaxy-ui:ro - galaxy-dev-ui-dist:/srv/galaxy-ui:ro
- galaxy-dev-site-dist:/srv/galaxy-site:ro
networks: networks:
- galaxy-internal - galaxy-internal
- edge - edge
@@ -266,5 +270,7 @@ volumes:
name: galaxy-dev-caddy-data name: galaxy-dev-caddy-data
galaxy-dev-ui-dist: galaxy-dev-ui-dist:
name: galaxy-dev-ui-dist name: galaxy-dev-ui-dist
galaxy-dev-site-dist:
name: galaxy-dev-site-dist
galaxy-dev-geoip-data: galaxy-dev-geoip-data:
name: galaxy-dev-geoip-data name: galaxy-dev-geoip-data
+9 -8
View File
@@ -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 `gitea.lan`; see project-level `CLAUDE.md`). It is also distinct from
the **long-lived dev environment** at the **long-lived dev environment** at
[`tools/dev-deploy/`](../dev-deploy/README.md), which is redeployed on [`tools/dev-deploy/`](../dev-deploy/README.md), which is redeployed on
every merge into `development` and is reachable as every merge into `development` and is reachable at the single origin
`https://www.galaxy.lan` / `https://api.galaxy.lan`. The two stacks `https://galaxy.lan` (site at `/`, game UI at `/game/`). The two stacks
(`tools/local-dev/` and `tools/dev-deploy/`) coexist on the same host (`tools/local-dev/` and `tools/dev-deploy/`) coexist on the same host
because every name — compose project, container, network, volume — is because every name — compose project, container, network, volume — is
distinct. distinct.
@@ -131,7 +131,7 @@ host compose network "galaxy-local-de
┌────────────────────────────────┐ ┌──────────────────────────────┐ ┌────────────────────────────────┐ ┌──────────────────────────────┐
│ browser localhost:5173 │── pnpm dev (Vite, host) ──┐ │ │ browser localhost:5173 │── pnpm dev (Vite, host) ──┐ │
│ ↳ /api/* proxied ───┼──────────────────────────▶│ gateway:8080 │ │ ↳ /api/* proxied ───┼──────────────────────────▶│ gateway:8080 │
│ ↳ /galaxy.gateway... ┼──────────────────────────▶│ │ ↳ /rpc/* proxied ───┼──────────────────────────▶│ gateway:9090
│ browser localhost:8025 │─────────────────────────▶│ mailpit:8025 │ │ browser localhost:8025 │─────────────────────────▶│ mailpit:8025 │
│ psql localhost:5433 │─────────────────────────▶│ postgres:5432 │ │ psql localhost:5433 │─────────────────────────▶│ postgres:5432 │
│ redis-cli localhost:6380 │─────────────────────────▶│ redis:6379 │ │ 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` Vite's dev server proxies `/api` (to the gateway REST listener) and
to the gateway, so every browser request stays same-origin (no CORS `/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 preflight). The gateway is therefore reachable only through Vite at
<http://localhost:5173>, not at <http://localhost:8080> from the <http://localhost:5173>, not at <http://localhost:8080> from the
browser tab. Direct curl/wget against <http://localhost:8080> still browser tab. Direct curl/wget against <http://localhost:8080> still
@@ -291,9 +292,9 @@ make status docker compose ps
## Relationship to other infrastructure ## Relationship to other infrastructure
- `tools/dev-deploy/` — long-lived dev environment redeployed on every - `tools/dev-deploy/` — long-lived dev environment redeployed on every
merge into `development`; reachable at `https://www.galaxy.lan` / merge into `development`; reachable at the single origin
`https://api.galaxy.lan`. Distinct compose project, container names, `https://galaxy.lan` (site at `/`, game UI at `/game/`). Distinct
network and volumes. compose project, container names, network and volumes.
- `integration/testenv/` — testcontainers harness used by - `integration/testenv/` — testcontainers harness used by
`make -C integration integration`. Uses the canonical `make -C integration integration`. Uses the canonical
`backend/Dockerfile` / `gateway/Dockerfile` at production defaults; `backend/Dockerfile` / `gateway/Dockerfile` at production defaults;
+2 -2
View File
@@ -180,7 +180,7 @@ services:
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST: "1000" 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_REQUESTS: "10000"
GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST: "1000" 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 # endpoints (ExecuteCommand, SubscribeEvents). The gateway's
# default for this class is 0 bytes, which rejects every # default for this class is 0 bytes, which rejects every
# non-empty body with HTTP 413; override with a generous limit # 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" GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_BURST: "1000"
ports: ports:
- "${LOCAL_DEV_GATEWAY_REST_PORT:-8080}:8080" - "${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 # browser reaches it via the Vite dev proxy in
# ui/frontend/vite.config.ts. # ui/frontend/vite.config.ts.
- "${LOCAL_DEV_GATEWAY_GRPC_PORT:-9090}:9090" - "${LOCAL_DEV_GATEWAY_GRPC_PORT:-9090}:9090"
+6 -6
View File
@@ -474,11 +474,11 @@ documents the historical labelling in its package doc.
Artifacts (delivered): Artifacts (delivered):
- `gateway/buf.gen.yaml` extended with `buf.build/connectrpc/go`, - `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` - `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 - 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` Connect handler interface, including a `grpc.ServerStreamingServer`
shim around `*connect.ServerStream[GatewayEvent]` and a gRPC shim around `*connect.ServerStream[GatewayEvent]` and a gRPC
`status.Error` → `*connect.Error` translation helper `status.Error` → `*connect.Error` translation helper
@@ -492,7 +492,7 @@ Artifacts (delivered):
`gateway/docs/runbook.md`, and `docs/ARCHITECTURE.md` §15 `gateway/docs/runbook.md`, and `docs/ARCHITECTURE.md` §15
- migrated tests: `gateway/internal/grpcapi/server_test.go`, - migrated tests: `gateway/internal/grpcapi/server_test.go`,
`test_fixtures_test.go`, and every `*_integration_test.go` in that `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 HTTP/2 cleartext loopback
- migrated harness: `integration/testenv/grpc_client.go` → - migrated harness: `integration/testenv/grpc_client.go` →
`connect_client.go`. `SignedGatewayClient` keeps the same public `connect_client.go`. `SignedGatewayClient` keeps the same public
@@ -580,10 +580,10 @@ Artifacts (delivered):
browsers; the JSDOM test path lives next to it in browsers; the JSDOM test path lives next to it in
`ui/frontend/tests/setup-wasm.ts`. `ui/frontend/tests/setup-wasm.ts`.
- `ui/frontend/src/api/connect.ts` — typed Connect-Web transport + - `ui/frontend/src/api/connect.ts` — typed Connect-Web transport +
`EdgeGatewayClient` factory. `GatewayClient` factory.
- `ui/frontend/src/api/galaxy-client.ts` — `GalaxyClient` skeleton - `ui/frontend/src/api/galaxy-client.ts` — `GalaxyClient` skeleton
with injected `Signer` and `Sha256` dependencies. 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) and `ui/frontend/src/proto/buf/validate/validate_pb.ts`
(generated as a transitive import via `--include-imports`). (generated as a transitive import via `--include-imports`).
- `ui/frontend/static/core.wasm` (903 KB) + `wasm_exec.js` (TinyGo - `ui/frontend/static/core.wasm` (903 KB) + `wasm_exec.js` (TinyGo
+1 -1
View File
@@ -1,6 +1,6 @@
// Package types defines the v1 transport envelopes carried over the // Package types defines the v1 transport envelopes carried over the
// wire between the Galaxy client and gateway. Envelope shapes mirror // 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 // 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 // them without depending on the protobuf runtime in WASM and gomobile
// builds. // builds.
+5 -4
View File
@@ -5,10 +5,11 @@
# Gateway public REST + Connect-Web edge listener. Points at the Vite # Gateway public REST + Connect-Web edge listener. Points at the Vite
# dev server's own origin so the browser sees same-origin requests; # dev server's own origin so the browser sees same-origin requests;
# Vite then proxies `/api` and `/galaxy.gateway.v1.EdgeGateway` to the # Vite then proxies `/api` to the REST listener (`:8080`) and `/rpc` to
# real gateway at `http://localhost:8080`. See `vite.config.ts`. To # the Connect listener (`:9090`, prefix stripped), mirroring the
# work against a non-local gateway, override the proxy target via # single-origin edge Caddy. See `vite.config.ts`. To work against a
# `VITE_DEV_PROXY_TARGET=http://gateway.host:8080 pnpm dev` (no UI # non-local gateway, override the proxy targets via
# `VITE_DEV_PROXY_TARGET` / `VITE_DEV_GRPC_PROXY_TARGET` (no UI
# rebuild needed). # rebuild needed).
VITE_GATEWAY_BASE_URL=http://localhost:5173 VITE_GATEWAY_BASE_URL=http://localhost:5173
+9 -8
View File
@@ -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 // gateway's authenticated edge surface. It speaks the Connect protocol
// over HTTP/1.1 (or HTTP/2 if the host upgrades the connection) — the // 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 // gateway listener built in Phase 4 natively serves Connect, gRPC, and
// gRPC-Web on the same h2c port. // gRPC-Web on the same h2c port.
// //
// The factory is intentionally thin: callers provide the gateway base // The factory is intentionally thin: callers provide the Connect base
// URL (e.g. https://api.galaxy.test), and receive a typed // URL (the same-origin `/rpc` prefix from `gatewayRpcBaseUrl`), and
// `EdgeGatewayClient`. Authentication, signing, and response // receive a typed
// `GatewayClient`. Authentication, signing, and response
// verification live one layer up, in `GalaxyClient`. // verification live one layer up, in `GalaxyClient`.
import { createClient, type Client } from "@connectrpc/connect"; import { createClient, type Client } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web"; 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<typeof EdgeGateway>; export type GatewayClient = Client<typeof Gateway>;
export function createEdgeGatewayClient(baseUrl: string): EdgeGatewayClient { export function createGatewayClient(baseUrl: string): GatewayClient {
return createClient(EdgeGateway, createConnectTransport({ baseUrl })); return createClient(Gateway, createConnectTransport({ baseUrl }));
} }
+8 -8
View File
@@ -26,10 +26,10 @@ import type { DeviceKeypair } from "../platform/store/index";
import { import {
SubscribeEventsRequestSchema, SubscribeEventsRequestSchema,
type GatewayEvent, type GatewayEvent,
} from "../proto/galaxy/gateway/v1/edge_gateway_pb"; } from "../proto/edge/v1/edge_gateway_pb";
import { GATEWAY_BASE_URL } from "../lib/env"; import { gatewayRpcBaseUrl } from "../lib/env";
import { session } from "../lib/session-store.svelte"; import { session } from "../lib/session-store.svelte";
import { createEdgeGatewayClient, type EdgeGatewayClient } from "./connect"; import { createGatewayClient, type GatewayClient } from "./connect";
const PROTOCOL_VERSION = "v1"; const PROTOCOL_VERSION = "v1";
const SUBSCRIBE_MESSAGE_TYPE = "gateway.subscribe"; const SUBSCRIBE_MESSAGE_TYPE = "gateway.subscribe";
@@ -83,7 +83,7 @@ export type ConnectionStatus =
* consumer cannot resolve by itself. Production code reads `core`, * consumer cannot resolve by itself. Production code reads `core`,
* `keypair`, and `deviceSessionId` from the session store and the * `keypair`, and `deviceSessionId` from the session store and the
* gateway public key from `lib/env`; tests inject a fake * 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. * backoff in fake-timer mode.
*/ */
export interface EventStreamStartOptions { export interface EventStreamStartOptions {
@@ -91,8 +91,8 @@ export interface EventStreamStartOptions {
keypair: DeviceKeypair; keypair: DeviceKeypair;
deviceSessionId: string; deviceSessionId: string;
gatewayResponsePublicKey: Uint8Array; gatewayResponsePublicKey: Uint8Array;
/** Custom transport client. Defaults to `createEdgeGatewayClient(GATEWAY_BASE_URL)`. */ /** Custom transport client. Defaults to `createGatewayClient(gatewayRpcBaseUrl())`. */
client?: EdgeGatewayClient; client?: GatewayClient;
/** Sleep hook for tests; defaults to a real-time `setTimeout`. */ /** Sleep hook for tests; defaults to a real-time `setTimeout`. */
sleep?: (ms: number) => Promise<void>; sleep?: (ms: number) => Promise<void>;
/** Random source for full-jitter backoff; defaults to `Math.random`. */ /** Random source for full-jitter backoff; defaults to `Math.random`. */
@@ -189,7 +189,7 @@ export class EventStream {
const sleep = opts.sleep ?? defaultSleep; const sleep = opts.sleep ?? defaultSleep;
const random = opts.random ?? Math.random; const random = opts.random ?? Math.random;
const onlineProbe = opts.onlineProbe ?? defaultOnlineProbe; const onlineProbe = opts.onlineProbe ?? defaultOnlineProbe;
const client = opts.client ?? createEdgeGatewayClient(GATEWAY_BASE_URL); const client = opts.client ?? createGatewayClient(gatewayRpcBaseUrl());
let attempt = 0; let attempt = 0;
while (!signal.aborted && this.running) { while (!signal.aborted && this.running) {
@@ -311,7 +311,7 @@ export class EventStream {
} }
async function openStream( async function openStream(
client: EdgeGatewayClient, client: GatewayClient,
opts: EventStreamStartOptions, opts: EventStreamStartOptions,
signal: AbortSignal, signal: AbortSignal,
): Promise<AsyncIterable<GatewayEvent>> { ): Promise<AsyncIterable<GatewayEvent>> {
+4 -4
View File
@@ -15,8 +15,8 @@ import type { Core } from "../platform/core/index";
import { import {
ExecuteCommandRequestSchema, ExecuteCommandRequestSchema,
type ExecuteCommandResponse, type ExecuteCommandResponse,
} from "../proto/galaxy/gateway/v1/edge_gateway_pb"; } from "../proto/edge/v1/edge_gateway_pb";
import type { EdgeGatewayClient } from "./connect"; import type { GatewayClient } from "./connect";
/** /**
* Signer produces a raw 64-byte Ed25519 signature over canonicalBytes. * Signer produces a raw 64-byte Ed25519 signature over canonicalBytes.
@@ -35,7 +35,7 @@ export type Sha256 = (payload: Uint8Array) => Promise<Uint8Array>;
export interface GalaxyClientOptions { export interface GalaxyClientOptions {
core: Core; core: Core;
edge: EdgeGatewayClient; edge: GatewayClient;
signer: Signer; signer: Signer;
sha256: Sha256; sha256: Sha256;
deviceSessionId: string; deviceSessionId: string;
@@ -53,7 +53,7 @@ export interface ExecuteCommandResult {
export class GalaxyClient { export class GalaxyClient {
private readonly core: Core; private readonly core: Core;
private readonly edge: EdgeGatewayClient; private readonly edge: GatewayClient;
private readonly signer: Signer; private readonly signer: Signer;
private readonly sha256: Sha256; private readonly sha256: Sha256;
private readonly deviceSessionId: string; private readonly deviceSessionId: string;
@@ -12,6 +12,7 @@ header now — we just hand the routes down as callbacks so the
viewer keeps its prop-driven contract. viewer keeps its prop-driven contract.
--> -->
<script lang="ts"> <script lang="ts">
import { withBase } from "$lib/paths";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
@@ -126,10 +127,10 @@ viewer keeps its prop-driven contract.
}); });
function backToReport() { function backToReport() {
goto(`/games/${gameId}/report`); goto(withBase(`/games/${gameId}/report`));
} }
function backToMap() { function backToMap() {
goto(`/games/${gameId}/map`); goto(withBase(`/games/${gameId}/map`));
} }
</script> </script>
@@ -25,6 +25,7 @@ fractions is a Phase 21 decision documented in
`ui/docs/science-designer-ux.md`. `ui/docs/science-designer-ux.md`.
--> -->
<script lang="ts"> <script lang="ts">
import { withBase } from "$lib/paths";
import { getContext, tick } from "svelte"; import { getContext, tick } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { page } from "$app/state"; import { page } from "$app/state";
@@ -125,7 +126,7 @@ fractions is a Phase 21 decision documented in
} }
function backToTable(): void { function backToTable(): void {
void goto(`/games/${gameId}/table/sciences`); void goto(withBase(`/games/${gameId}/table/sciences`));
} }
async function save(): Promise<void> { async function save(): Promise<void> {
+3 -2
View File
@@ -20,6 +20,7 @@ Phase 29 wires the wrap-mode toggle on top of the per-game `wrapMode`
preference the store already manages. preference the store already manages.
--> -->
<script lang="ts"> <script lang="ts">
import { withBase } from "$lib/paths";
import { getContext, onDestroy, onMount, untrack } from "svelte"; import { getContext, onDestroy, onMount, untrack } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { page } from "$app/state"; import { page } from "$app/state";
@@ -636,14 +637,14 @@ preference the store already manages.
const gameId = page.params.id ?? ""; const gameId = page.params.id ?? "";
const turn = store?.report?.turn ?? 0; const turn = store?.report?.turn ?? 0;
void goto( void goto(
`/games/${gameId}/battle/${target.battleId}?turn=${turn}`, withBase(`/games/${gameId}/battle/${target.battleId}?turn=${turn}`),
); );
break; break;
} }
case "bombing": { case "bombing": {
const gameId = page.params.id ?? ""; const gameId = page.params.id ?? "";
void goto( void goto(
`/games/${gameId}/report#report-bombings`, withBase(`/games/${gameId}/report#report-bombings`),
).then(() => { ).then(() => {
if (typeof document === "undefined") return; if (typeof document === "undefined") return;
const row = document.querySelector( const row = document.querySelector(
@@ -20,6 +20,7 @@ The active section is computed by the orchestrator
`activeSlug` prop. The TOC itself owns no observers. `activeSlug` prop. The TOC itself owns no observers.
--> -->
<script lang="ts"> <script lang="ts">
import { withBase } from "$lib/paths";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte"; import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
@@ -62,7 +63,7 @@ The active section is computed by the orchestrator
} }
async function backToMap(): Promise<void> { async function backToMap(): Promise<void> {
await goto(`/games/${gameId}/map`); await goto(withBase(`/games/${gameId}/map`));
} }
</script> </script>
@@ -7,6 +7,7 @@ monospace `<span>`; the rewire here is the one-liner the Phase 23
decision log called out. decision log called out.
--> -->
<script lang="ts"> <script lang="ts">
import { withBase } from "$lib/paths";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { page } from "$app/state"; import { page } from "$app/state";
@@ -47,7 +48,7 @@ decision log called out.
</span> </span>
<a <a
class="uuid" class="uuid"
href={`/games/${gameId}/battle/${b.id}?turn=${turn}`} href={withBase(`/games/${gameId}/battle/${b.id}?turn=${turn}`)}
data-testid="report-battle-row" data-testid="report-battle-row"
data-id={b.id} data-id={b.id}
>{b.id}</a> >{b.id}</a>
@@ -17,6 +17,7 @@ The component sits inside the active-view slot owned by
data fetching is performed here — the layout is responsible. data fetching is performed here — the layout is responsible.
--> -->
<script lang="ts"> <script lang="ts">
import { withBase } from "$lib/paths";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { page } from "$app/state"; import { page } from "$app/state";
@@ -117,11 +118,11 @@ data fetching is performed here — the layout is responsible.
} }
function openDesigner(name: string): void { function openDesigner(name: string): void {
void goto(`/games/${gameId}/designer/science/${encodeURIComponent(name)}`); void goto(withBase(`/games/${gameId}/designer/science/${encodeURIComponent(name)}`));
} }
function newScience(): void { function newScience(): void {
void goto(`/games/${gameId}/designer/science`); void goto(withBase(`/games/${gameId}/designer/science`));
} }
async function deleteScience(name: string): Promise<void> { async function deleteScience(name: string): Promise<void> {
+23 -3
View File
@@ -3,9 +3,14 @@
// at the first import. // at the first import.
// //
// `VITE_GATEWAY_BASE_URL` is the base URL of the gateway public REST // `VITE_GATEWAY_BASE_URL` is the base URL of the gateway public REST
// surface and the Connect-Web authenticated edge (same host, same // surface. An empty value means "same origin": the single-origin
// port; the gateway listener serves both). It defaults to the local // deployment serves the UI, the REST surface (`/api/...`), and the
// dev address used by `tools/local-ci` and the integration suite. // authenticated Connect-Web edge (`/rpc/...`) behind one host, so the
// browser issues same-origin requests and needs no absolute base. A
// non-empty value (the Vite dev proxy, `tools/local-ci`, the
// integration suite) points REST at that absolute host instead. The
// Connect base is derived from this value by `gatewayRpcBaseUrl`,
// which appends the `/rpc` routing prefix.
// //
// `VITE_GATEWAY_RESPONSE_PUBLIC_KEY` is the gateway's response-signing // `VITE_GATEWAY_RESPONSE_PUBLIC_KEY` is the gateway's response-signing
// Ed25519 public key, encoded as standard (non-URL-safe) base64 of // Ed25519 public key, encoded as standard (non-URL-safe) base64 of
@@ -26,6 +31,21 @@ const RAW_RESPONSE_PUBLIC_KEY: string =
export const GATEWAY_BASE_URL: string = stripTrailingSlash(RAW_BASE_URL); export const GATEWAY_BASE_URL: string = stripTrailingSlash(RAW_BASE_URL);
/**
* gatewayRpcBaseUrl is the base URL for the authenticated Connect-Web
* surface. The edge Caddy and the Vite dev proxy route the `/rpc`
* prefix to the gateway's Connect listener, stripping it before the
* request reaches the proto-derived `edge.v1.Gateway` service path.
* When `GATEWAY_BASE_URL` is empty the gateway shares the document
* origin, so the origin is resolved at call time from `window`.
*/
export function gatewayRpcBaseUrl(): string {
const origin =
GATEWAY_BASE_URL ||
(typeof window !== "undefined" ? window.location.origin : "");
return `${origin}/rpc`;
}
export const GATEWAY_RESPONSE_PUBLIC_KEY: Uint8Array = decodeBase64( export const GATEWAY_RESPONSE_PUBLIC_KEY: Uint8Array = decodeBase64(
RAW_RESPONSE_PUBLIC_KEY, RAW_RESPONSE_PUBLIC_KEY,
); );
+2 -1
View File
@@ -12,6 +12,7 @@ navigation. Phase 26 introduces the history-mode entry; Phase 35
polishes microcopy. polishes microcopy.
--> -->
<script lang="ts"> <script lang="ts">
import { withBase } from "$lib/paths";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte"; import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
@@ -41,7 +42,7 @@ polishes microcopy.
function go(path: string): void { function go(path: string): void {
open = false; open = false;
void goto(path); void goto(withBase(path));
} }
function onKeyDown(event: KeyboardEvent): void { function onKeyDown(event: KeyboardEvent): void {
+25
View File
@@ -0,0 +1,25 @@
// Base-path helpers for the single-origin deployment. The game UI is
// served under `kit.paths.base` — empty at the root for local dev,
// vitest, and Playwright, and `/game` in the deployed single-origin
// build. SvelteKit does not auto-prefix `goto`, `<a href>`, raw asset
// fetches, or the service-worker scope, so every app-internal absolute
// path is routed through `withBase`.
//
// `base` from `$app/paths` is the low-level primitive that `resolve()`
// builds on. We use it directly here (rather than `resolve()`) because
// the client navigates to and fetches dynamic, runtime-built paths
// (command routes, `core.wasm`, the service worker) that `resolve()`'s
// statically-typed route-id surface cannot express.
import { base } from "$app/paths";
/** appBase is the configured base path (empty string at the root). */
export const appBase = base;
/**
* withBase prefixes an app-internal absolute path (leading slash) with
* the configured base path. At the root it returns the path unchanged;
* under the single-origin deployment it yields e.g. `/game/lobby`.
*/
export function withBase(path: string): string {
return `${base}${path}`;
}
@@ -13,6 +13,7 @@ exists; until then the convenience of one source of truth for
destinations beats the duplication. destinations beats the duplication.
--> -->
<script lang="ts"> <script lang="ts">
import { withBase } from "$lib/paths";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte"; import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
@@ -47,13 +48,13 @@ destinations beats the duplication.
async function selectTool(tool: MobileTool): Promise<void> { async function selectTool(tool: MobileTool): Promise<void> {
moreOpen = false; moreOpen = false;
onSelectTool(tool); onSelectTool(tool);
await goto(`/games/${gameId}/map`); await goto(withBase(`/games/${gameId}/map`));
} }
async function go(path: string): Promise<void> { async function go(path: string): Promise<void> {
moreOpen = false; moreOpen = false;
onSelectTool("map"); onSelectTool("map");
await goto(path); await goto(withBase(path));
} }
function toggleMore(): void { function toggleMore(): void {
+3 -2
View File
@@ -34,6 +34,7 @@ import type {
WeaponsBlockInput, WeaponsBlockInput,
WeaponsForAttackInput, WeaponsForAttackInput,
} from "./index"; } from "./index";
import { withBase } from "$lib/paths";
/** /**
* GalaxyCoreBridge is the shape Go installs on `globalThis.galaxyCore`. * GalaxyCoreBridge is the shape Go installs on `globalThis.galaxyCore`.
@@ -143,7 +144,7 @@ async function bootBrowserWasm(): Promise<Core> {
throw new Error("loadWasmCore: Go runtime missing after wasm_exec.js load"); throw new Error("loadWasmCore: Go runtime missing after wasm_exec.js load");
} }
const go = new Go(); const go = new Go();
const response = await fetch("/core.wasm"); const response = await fetch(withBase("/core.wasm"));
const bytes = await response.arrayBuffer(); const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes, go.importObject); const { instance } = await WebAssembly.instantiate(bytes, go.importObject);
void go.run(instance); void go.run(instance);
@@ -156,7 +157,7 @@ async function ensureGoRuntimeLoaded(): Promise<void> {
} }
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const script = document.createElement("script"); const script = document.createElement("script");
script.src = "/wasm_exec.js"; script.src = withBase("/wasm_exec.js");
script.onload = () => resolve(); script.onload = () => resolve();
script.onerror = () => reject(new Error("failed to load /wasm_exec.js")); script.onerror = () => reject(new Error("failed to load /wasm_exec.js"));
document.head.appendChild(script); document.head.appendChild(script);
@@ -1,22 +1,22 @@
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts" // @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file galaxy/gateway/v1/edge_gateway.proto (package galaxy.gateway.v1, syntax proto3) // @generated from file edge/v1/edge_gateway.proto (package edge.v1, syntax proto3)
/* eslint-disable */ /* eslint-disable */
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; import { file_buf_validate_validate } from "../../buf/validate/validate_pb";
import type { Message } from "@bufbuild/protobuf"; import type { Message } from "@bufbuild/protobuf";
/** /**
* Describes the file galaxy/gateway/v1/edge_gateway.proto. * Describes the file edge/v1/edge_gateway.proto.
*/ */
export const file_galaxy_gateway_v1_edge_gateway: GenFile = /*@__PURE__*/ export const file_edge_v1_edge_gateway: GenFile = /*@__PURE__*/
fileDesc("CiRnYWxheHkvZ2F0ZXdheS92MS9lZGdlX2dhdGV3YXkucHJvdG8SEWdhbGF4eS5nYXRld2F5LnYxIqYCChVFeGVjdXRlQ29tbWFuZFJlcXVlc3QSIQoQcHJvdG9jb2xfdmVyc2lvbhgBIAEoCUIHukgEcgIQARIiChFkZXZpY2Vfc2Vzc2lvbl9pZBgCIAEoCUIHukgEcgIQARIdCgxtZXNzYWdlX3R5cGUYAyABKAlCB7pIBHICEAESHQoMdGltZXN0YW1wX21zGAQgASgDQge6SAQiAiAAEhsKCnJlcXVlc3RfaWQYBSABKAlCB7pIBHICEAESHgoNcGF5bG9hZF9ieXRlcxgGIAEoDEIHukgEegIQARIdCgxwYXlsb2FkX2hhc2gYByABKAxCB7pIBHoCEAESGgoJc2lnbmF0dXJlGAggASgMQge6SAR6AhABEhAKCHRyYWNlX2lkGAkgASgJIrEBChZFeGVjdXRlQ29tbWFuZFJlc3BvbnNlEhgKEHByb3RvY29sX3ZlcnNpb24YASABKAkSEgoKcmVxdWVzdF9pZBgCIAEoCRIUCgx0aW1lc3RhbXBfbXMYAyABKAMSEwoLcmVzdWx0X2NvZGUYBCABKAkSFQoNcGF5bG9hZF9ieXRlcxgFIAEoDBIUCgxwYXlsb2FkX2hhc2gYBiABKAwSEQoJc2lnbmF0dXJlGAcgASgMIp4CChZTdWJzY3JpYmVFdmVudHNSZXF1ZXN0EiEKEHByb3RvY29sX3ZlcnNpb24YASABKAlCB7pIBHICEAESIgoRZGV2aWNlX3Nlc3Npb25faWQYAiABKAlCB7pIBHICEAESHQoMbWVzc2FnZV90eXBlGAMgASgJQge6SARyAhABEh0KDHRpbWVzdGFtcF9tcxgEIAEoA0IHukgEIgIgABIbCgpyZXF1ZXN0X2lkGAUgASgJQge6SARyAhABEh0KDHBheWxvYWRfaGFzaBgGIAEoDEIHukgEegIQARIaCglzaWduYXR1cmUYByABKAxCB7pIBHoCEAESFQoNcGF5bG9hZF9ieXRlcxgIIAEoDBIQCgh0cmFjZV9pZBgJIAEoCSKwAQoMR2F0ZXdheUV2ZW50EhIKCmV2ZW50X3R5cGUYASABKAkSEAoIZXZlbnRfaWQYAiABKAkSFAoMdGltZXN0YW1wX21zGAMgASgDEhUKDXBheWxvYWRfYnl0ZXMYBCABKAwSFAoMcGF5bG9hZF9oYXNoGAUgASgMEhEKCXNpZ25hdHVyZRgGIAEoDBISCgpyZXF1ZXN0X2lkGAcgASgJEhAKCHRyYWNlX2lkGAggASgJMtUBCgtFZGdlR2F0ZXdheRJlCg5FeGVjdXRlQ29tbWFuZBIoLmdhbGF4eS5nYXRld2F5LnYxLkV4ZWN1dGVDb21tYW5kUmVxdWVzdBopLmdhbGF4eS5nYXRld2F5LnYxLkV4ZWN1dGVDb21tYW5kUmVzcG9uc2USXwoPU3Vic2NyaWJlRXZlbnRzEikuZ2FsYXh5LmdhdGV3YXkudjEuU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBofLmdhbGF4eS5nYXRld2F5LnYxLkdhdGV3YXlFdmVudDABQjJaMGdhbGF4eS9nYXRld2F5L3Byb3RvL2dhbGF4eS9nYXRld2F5L3YxO2dhdGV3YXl2MWIGcHJvdG8z", [file_buf_validate_validate]); fileDesc("ChplZGdlL3YxL2VkZ2VfZ2F0ZXdheS5wcm90bxIHZWRnZS52MSKmAgoVRXhlY3V0ZUNvbW1hbmRSZXF1ZXN0EiEKEHByb3RvY29sX3ZlcnNpb24YASABKAlCB7pIBHICEAESIgoRZGV2aWNlX3Nlc3Npb25faWQYAiABKAlCB7pIBHICEAESHQoMbWVzc2FnZV90eXBlGAMgASgJQge6SARyAhABEh0KDHRpbWVzdGFtcF9tcxgEIAEoA0IHukgEIgIgABIbCgpyZXF1ZXN0X2lkGAUgASgJQge6SARyAhABEh4KDXBheWxvYWRfYnl0ZXMYBiABKAxCB7pIBHoCEAESHQoMcGF5bG9hZF9oYXNoGAcgASgMQge6SAR6AhABEhoKCXNpZ25hdHVyZRgIIAEoDEIHukgEegIQARIQCgh0cmFjZV9pZBgJIAEoCSKxAQoWRXhlY3V0ZUNvbW1hbmRSZXNwb25zZRIYChBwcm90b2NvbF92ZXJzaW9uGAEgASgJEhIKCnJlcXVlc3RfaWQYAiABKAkSFAoMdGltZXN0YW1wX21zGAMgASgDEhMKC3Jlc3VsdF9jb2RlGAQgASgJEhUKDXBheWxvYWRfYnl0ZXMYBSABKAwSFAoMcGF5bG9hZF9oYXNoGAYgASgMEhEKCXNpZ25hdHVyZRgHIAEoDCKeAgoWU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBIhChBwcm90b2NvbF92ZXJzaW9uGAEgASgJQge6SARyAhABEiIKEWRldmljZV9zZXNzaW9uX2lkGAIgASgJQge6SARyAhABEh0KDG1lc3NhZ2VfdHlwZRgDIAEoCUIHukgEcgIQARIdCgx0aW1lc3RhbXBfbXMYBCABKANCB7pIBCICIAASGwoKcmVxdWVzdF9pZBgFIAEoCUIHukgEcgIQARIdCgxwYXlsb2FkX2hhc2gYBiABKAxCB7pIBHoCEAESGgoJc2lnbmF0dXJlGAcgASgMQge6SAR6AhABEhUKDXBheWxvYWRfYnl0ZXMYCCABKAwSEAoIdHJhY2VfaWQYCSABKAkisAEKDEdhdGV3YXlFdmVudBISCgpldmVudF90eXBlGAEgASgJEhAKCGV2ZW50X2lkGAIgASgJEhQKDHRpbWVzdGFtcF9tcxgDIAEoAxIVCg1wYXlsb2FkX2J5dGVzGAQgASgMEhQKDHBheWxvYWRfaGFzaBgFIAEoDBIRCglzaWduYXR1cmUYBiABKAwSEgoKcmVxdWVzdF9pZBgHIAEoCRIQCgh0cmFjZV9pZBgIIAEoCTKpAQoHR2F0ZXdheRJRCg5FeGVjdXRlQ29tbWFuZBIeLmVkZ2UudjEuRXhlY3V0ZUNvbW1hbmRSZXF1ZXN0Gh8uZWRnZS52MS5FeGVjdXRlQ29tbWFuZFJlc3BvbnNlEksKD1N1YnNjcmliZUV2ZW50cxIfLmVkZ2UudjEuU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBoVLmVkZ2UudjEuR2F0ZXdheUV2ZW50MAFCJVojZ2FsYXh5L2dhdGV3YXkvcHJvdG8vZWRnZS92MTtlZGdldjFiBnByb3RvMw", [file_buf_validate_validate]);
/** /**
* @generated from message galaxy.gateway.v1.ExecuteCommandRequest * @generated from message edge.v1.ExecuteCommandRequest
*/ */
export type ExecuteCommandRequest = Message<"galaxy.gateway.v1.ExecuteCommandRequest"> & { export type ExecuteCommandRequest = Message<"edge.v1.ExecuteCommandRequest"> & {
/** /**
* protocol_version identifies the request envelope version. The gateway * protocol_version identifies the request envelope version. The gateway
* accepts only the literal "v1" after required-field validation succeeds. * accepts only the literal "v1" after required-field validation succeeds.
@@ -69,16 +69,16 @@ export type ExecuteCommandRequest = Message<"galaxy.gateway.v1.ExecuteCommandReq
}; };
/** /**
* Describes the message galaxy.gateway.v1.ExecuteCommandRequest. * Describes the message edge.v1.ExecuteCommandRequest.
* Use `create(ExecuteCommandRequestSchema)` to create a new message. * Use `create(ExecuteCommandRequestSchema)` to create a new message.
*/ */
export const ExecuteCommandRequestSchema: GenMessage<ExecuteCommandRequest> = /*@__PURE__*/ export const ExecuteCommandRequestSchema: GenMessage<ExecuteCommandRequest> = /*@__PURE__*/
messageDesc(file_galaxy_gateway_v1_edge_gateway, 0); messageDesc(file_edge_v1_edge_gateway, 0);
/** /**
* @generated from message galaxy.gateway.v1.ExecuteCommandResponse * @generated from message edge.v1.ExecuteCommandResponse
*/ */
export type ExecuteCommandResponse = Message<"galaxy.gateway.v1.ExecuteCommandResponse"> & { export type ExecuteCommandResponse = Message<"edge.v1.ExecuteCommandResponse"> & {
/** /**
* @generated from field: string protocol_version = 1; * @generated from field: string protocol_version = 1;
*/ */
@@ -116,16 +116,16 @@ export type ExecuteCommandResponse = Message<"galaxy.gateway.v1.ExecuteCommandRe
}; };
/** /**
* Describes the message galaxy.gateway.v1.ExecuteCommandResponse. * Describes the message edge.v1.ExecuteCommandResponse.
* Use `create(ExecuteCommandResponseSchema)` to create a new message. * Use `create(ExecuteCommandResponseSchema)` to create a new message.
*/ */
export const ExecuteCommandResponseSchema: GenMessage<ExecuteCommandResponse> = /*@__PURE__*/ export const ExecuteCommandResponseSchema: GenMessage<ExecuteCommandResponse> = /*@__PURE__*/
messageDesc(file_galaxy_gateway_v1_edge_gateway, 1); messageDesc(file_edge_v1_edge_gateway, 1);
/** /**
* @generated from message galaxy.gateway.v1.SubscribeEventsRequest * @generated from message edge.v1.SubscribeEventsRequest
*/ */
export type SubscribeEventsRequest = Message<"galaxy.gateway.v1.SubscribeEventsRequest"> & { export type SubscribeEventsRequest = Message<"edge.v1.SubscribeEventsRequest"> & {
/** /**
* protocol_version identifies the request envelope version. The gateway * protocol_version identifies the request envelope version. The gateway
* accepts only the literal "v1" after required-field validation succeeds. * accepts only the literal "v1" after required-field validation succeeds.
@@ -179,16 +179,16 @@ export type SubscribeEventsRequest = Message<"galaxy.gateway.v1.SubscribeEventsR
}; };
/** /**
* Describes the message galaxy.gateway.v1.SubscribeEventsRequest. * Describes the message edge.v1.SubscribeEventsRequest.
* Use `create(SubscribeEventsRequestSchema)` to create a new message. * Use `create(SubscribeEventsRequestSchema)` to create a new message.
*/ */
export const SubscribeEventsRequestSchema: GenMessage<SubscribeEventsRequest> = /*@__PURE__*/ export const SubscribeEventsRequestSchema: GenMessage<SubscribeEventsRequest> = /*@__PURE__*/
messageDesc(file_galaxy_gateway_v1_edge_gateway, 2); messageDesc(file_edge_v1_edge_gateway, 2);
/** /**
* @generated from message galaxy.gateway.v1.GatewayEvent * @generated from message edge.v1.GatewayEvent
*/ */
export type GatewayEvent = Message<"galaxy.gateway.v1.GatewayEvent"> & { export type GatewayEvent = Message<"edge.v1.GatewayEvent"> & {
/** /**
* @generated from field: string event_type = 1; * @generated from field: string event_type = 1;
*/ */
@@ -231,18 +231,18 @@ export type GatewayEvent = Message<"galaxy.gateway.v1.GatewayEvent"> & {
}; };
/** /**
* Describes the message galaxy.gateway.v1.GatewayEvent. * Describes the message edge.v1.GatewayEvent.
* Use `create(GatewayEventSchema)` to create a new message. * Use `create(GatewayEventSchema)` to create a new message.
*/ */
export const GatewayEventSchema: GenMessage<GatewayEvent> = /*@__PURE__*/ export const GatewayEventSchema: GenMessage<GatewayEvent> = /*@__PURE__*/
messageDesc(file_galaxy_gateway_v1_edge_gateway, 3); messageDesc(file_edge_v1_edge_gateway, 3);
/** /**
* @generated from service galaxy.gateway.v1.EdgeGateway * @generated from service edge.v1.Gateway
*/ */
export const EdgeGateway: GenService<{ export const Gateway: GenService<{
/** /**
* @generated from rpc galaxy.gateway.v1.EdgeGateway.ExecuteCommand * @generated from rpc edge.v1.Gateway.ExecuteCommand
*/ */
executeCommand: { executeCommand: {
methodKind: "unary"; methodKind: "unary";
@@ -250,7 +250,7 @@ export const EdgeGateway: GenService<{
output: typeof ExecuteCommandResponseSchema; output: typeof ExecuteCommandResponseSchema;
}, },
/** /**
* @generated from rpc galaxy.gateway.v1.EdgeGateway.SubscribeEvents * @generated from rpc edge.v1.Gateway.SubscribeEvents
*/ */
subscribeEvents: { subscribeEvents: {
methodKind: "server_streaming"; methodKind: "server_streaming";
@@ -258,5 +258,5 @@ export const EdgeGateway: GenService<{
output: typeof GatewayEventSchema; output: typeof GatewayEventSchema;
}, },
}> = /*@__PURE__*/ }> = /*@__PURE__*/
serviceDesc(file_galaxy_gateway_v1_edge_gateway, 0); serviceDesc(file_edge_v1_edge_gateway, 0);
+9 -4
View File
@@ -5,6 +5,7 @@
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { page } from "$app/state"; import { page } from "$app/state";
import { dev } from "$app/environment"; import { dev } from "$app/environment";
import { appBase, withBase } from "$lib/paths";
import { i18n } from "$lib/i18n/index.svelte"; import { i18n } from "$lib/i18n/index.svelte";
import { session } from "$lib/session-store.svelte"; import { session } from "$lib/session-store.svelte";
import { eventStream } from "../api/events.svelte"; import { eventStream } from "../api/events.svelte";
@@ -26,7 +27,9 @@
// in svelte.config.js) so `vite dev` and the dev-server e2e suite // in svelte.config.js) so `vite dev` and the dev-server e2e suite
// run without the worker intercepting requests. // run without the worker intercepting requests.
if (!dev && "serviceWorker" in navigator) { if (!dev && "serviceWorker" in navigator) {
void navigator.serviceWorker.register("/service-worker.js"); void navigator.serviceWorker.register(withBase("/service-worker.js"), {
scope: withBase("/"),
});
} }
return () => { return () => {
eventStream.stop(); eventStream.stop();
@@ -75,7 +78,9 @@
streamSessionId = null; streamSessionId = null;
} }
const pathname = page.url.pathname; // page.url.pathname includes the configured base path; strip it so
// the route comparisons below stay base-agnostic.
const pathname = page.url.pathname.slice(appBase.length);
// Debug-only routes under /__debug/* run their own bootstrap // Debug-only routes under /__debug/* run their own bootstrap
// path against the storage primitives and must bypass the // path against the storage primitives and must bypass the
// auth guard so Phase 6's Playwright spec can drive the // auth guard so Phase 6's Playwright spec can drive the
@@ -84,9 +89,9 @@
return; return;
} }
if (session.status === "anonymous" && pathname !== "/login") { if (session.status === "anonymous" && pathname !== "/login") {
void goto("/login", { replaceState: true }); void goto(withBase("/login"), { replaceState: true });
} else if (session.status === "authenticated" && pathname === "/login") { } else if (session.status === "authenticated" && pathname === "/login") {
void goto("/lobby", { replaceState: true }); void goto(withBase("/lobby"), { replaceState: true });
} }
}); });
</script> </script>
@@ -43,6 +43,7 @@ the next game's snapshot — and the next game's selection — start
fresh. fresh.
--> -->
<script lang="ts"> <script lang="ts">
import { withBase } from "$lib/paths";
import { onDestroy, onMount, setContext, untrack } from "svelte"; import { onDestroy, onMount, setContext, untrack } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { page } from "$app/state"; import { page } from "$app/state";
@@ -86,9 +87,9 @@ fresh.
import { session } from "$lib/session-store.svelte"; import { session } from "$lib/session-store.svelte";
import { loadStore } from "../../../platform/store/index"; import { loadStore } from "../../../platform/store/index";
import { loadCore } from "../../../platform/core/index"; import { loadCore } from "../../../platform/core/index";
import { createEdgeGatewayClient } from "../../../api/connect"; import { createGatewayClient } from "../../../api/connect";
import { GalaxyClient } from "../../../api/galaxy-client"; import { GalaxyClient } from "../../../api/galaxy-client";
import { GATEWAY_BASE_URL, GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env"; import { gatewayRpcBaseUrl, GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env";
import { import {
getSyntheticReport, getSyntheticReport,
isSyntheticGameId, isSyntheticGameId,
@@ -373,7 +374,7 @@ fresh.
if (isSyntheticGameId(gameId)) { if (isSyntheticGameId(gameId)) {
const report = getSyntheticReport(gameId); const report = getSyntheticReport(gameId);
if (report === undefined) { if (report === undefined) {
await goto("/lobby"); await goto(withBase("/lobby"));
return; return;
} }
try { try {
@@ -420,7 +421,7 @@ fresh.
coreHolder.set(core); coreHolder.set(core);
const client = new GalaxyClient({ const client = new GalaxyClient({
core, core,
edge: createEdgeGatewayClient(GATEWAY_BASE_URL), edge: createGatewayClient(gatewayRpcBaseUrl()),
signer: (canonical) => keypair.sign(canonical), signer: (canonical) => keypair.sign(canonical),
sha256, sha256,
deviceSessionId, deviceSessionId,
@@ -472,7 +473,7 @@ fresh.
messageParams: { from: parsed.from }, messageParams: { from: parsed.from },
actionLabelKey: "game.events.mail_new.action", actionLabelKey: "game.events.mail_new.action",
onAction: () => { onAction: () => {
void goto(`/games/${gameId}/mail`); void goto(withBase(`/games/${gameId}/mail`));
}, },
durationMs: 8000, durationMs: 8000,
}); });
+7 -6
View File
@@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import { withBase } from "$lib/paths";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { createEdgeGatewayClient } from "../../api/connect"; import { createGatewayClient } from "../../api/connect";
import { GalaxyClient } from "../../api/galaxy-client"; import { GalaxyClient } from "../../api/galaxy-client";
import { import {
LobbyError, LobbyError,
@@ -19,7 +20,7 @@
} from "../../api/lobby"; } from "../../api/lobby";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { AccountResponse } from "../../proto/galaxy/fbs/user"; import { AccountResponse } from "../../proto/galaxy/fbs/user";
import { GATEWAY_BASE_URL, GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env"; import { gatewayRpcBaseUrl, GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env";
import { import {
SyntheticReportError, SyntheticReportError,
loadSyntheticReportFromJSON, loadSyntheticReportFromJSON,
@@ -184,11 +185,11 @@
} }
function gotoCreate(): void { function gotoCreate(): void {
goto("/lobby/create"); goto(withBase("/lobby/create"));
} }
function gotoGame(gameId: string): void { function gotoGame(gameId: string): void {
goto(`/games/${gameId}/map`); goto(withBase(`/games/${gameId}/map`));
} }
async function onSyntheticFileChange( async function onSyntheticFileChange(
@@ -207,7 +208,7 @@
const text = await file.text(); const text = await file.text();
const json: unknown = JSON.parse(text); const json: unknown = JSON.parse(text);
const { gameId } = loadSyntheticReportFromJSON(json); const { gameId } = loadSyntheticReportFromJSON(json);
await goto(`/games/${gameId}/map`); await goto(withBase(`/games/${gameId}/map`));
} catch (err) { } catch (err) {
if (err instanceof SyntheticReportError) { if (err instanceof SyntheticReportError) {
syntheticError = err.message; syntheticError = err.message;
@@ -250,7 +251,7 @@
const core = await loadCore(); const core = await loadCore();
client = new GalaxyClient({ client = new GalaxyClient({
core, core,
edge: createEdgeGatewayClient(GATEWAY_BASE_URL), edge: createGatewayClient(gatewayRpcBaseUrl()),
signer: (canonical) => keypair.sign(canonical), signer: (canonical) => keypair.sign(canonical),
sha256, sha256,
deviceSessionId: session.deviceSessionId, deviceSessionId: session.deviceSessionId,
@@ -1,11 +1,12 @@
<script lang="ts"> <script lang="ts">
import { withBase } from "$lib/paths";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { createEdgeGatewayClient } from "../../../api/connect"; import { createGatewayClient } from "../../../api/connect";
import { GalaxyClient } from "../../../api/galaxy-client"; import { GalaxyClient } from "../../../api/galaxy-client";
import { LobbyError, createGame } from "../../../api/lobby"; import { LobbyError, createGame } from "../../../api/lobby";
import { GATEWAY_BASE_URL, GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env"; import { gatewayRpcBaseUrl, GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte"; import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
import { loadCore } from "../../../platform/core/index"; import { loadCore } from "../../../platform/core/index";
import { session } from "$lib/session-store.svelte"; import { session } from "$lib/session-store.svelte";
@@ -51,7 +52,7 @@
} }
function cancel(): void { function cancel(): void {
goto("/lobby"); goto(withBase("/lobby"));
} }
async function submit(): Promise<void> { async function submit(): Promise<void> {
@@ -93,7 +94,7 @@
turnSchedule: trimmedSchedule, turnSchedule: trimmedSchedule,
targetEngineVersion: targetEngineVersion.trim() || DEFAULT_TARGET_ENGINE_VERSION, targetEngineVersion: targetEngineVersion.trim() || DEFAULT_TARGET_ENGINE_VERSION,
}); });
goto("/lobby"); goto(withBase("/lobby"));
} catch (err) { } catch (err) {
formError = describeLobbyError(err); formError = describeLobbyError(err);
} finally { } finally {
@@ -116,7 +117,7 @@
const core = await loadCore(); const core = await loadCore();
client = new GalaxyClient({ client = new GalaxyClient({
core, core,
edge: createEdgeGatewayClient(GATEWAY_BASE_URL), edge: createGatewayClient(gatewayRpcBaseUrl()),
signer: (canonical) => keypair.sign(canonical), signer: (canonical) => keypair.sign(canonical),
sha256, sha256,
deviceSessionId: session.deviceSessionId, deviceSessionId: session.deviceSessionId,
+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { withBase } from "$lib/paths";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { import {
AuthError, AuthError,
@@ -88,7 +89,7 @@
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
}); });
await session.signIn(result.deviceSessionId); await session.signIn(result.deviceSessionId);
void goto("/lobby", { replaceState: true }); void goto(withBase("/lobby"), { replaceState: true });
} catch (err) { } catch (err) {
if (err instanceof AuthError && err.code === "invalid_request") { if (err instanceof AuthError && err.code === "invalid_request") {
challengeId = null; challengeId = null;
+7 -5
View File
@@ -9,14 +9,16 @@
// //
// SvelteKit registers this worker automatically in the production build. // SvelteKit registers this worker automatically in the production build.
import { build, files, version } from "$service-worker"; import { base, build, files, version } from "$service-worker";
const sw = self as unknown as ServiceWorkerGlobalScope; const sw = self as unknown as ServiceWorkerGlobalScope;
const CACHE = `galaxy-cache-${version}`; const CACHE = `galaxy-cache-${version}`;
// "/" is the SPA shell (adapter-static fallback); precaching it makes the // `${base}/` is the SPA shell (adapter-static fallback); precaching it
// start_url load offline. // makes the start_url load offline. `base` is empty at the root and
const PRECACHE = ["/", ...build, ...files]; // `/game` under the single-origin deployment, and `$service-worker`
// derives it from `location.pathname` so it stays correct in a subdir.
const PRECACHE = [`${base}/`, ...build, ...files];
sw.addEventListener("install", (event) => { sw.addEventListener("install", (event) => {
event.waitUntil( event.waitUntil(
@@ -65,7 +67,7 @@ sw.addEventListener("fetch", (event) => {
const cached = await cache.match(request); const cached = await cache.match(request);
if (cached) return cached; if (cached) return cached;
if (request.mode === "navigate") { if (request.mode === "navigate") {
const shell = await cache.match("/"); const shell = await cache.match(`${base}/`);
if (shell) return shell; if (shell) return shell;
} }
throw err; throw err;
+6 -6
View File
@@ -2,28 +2,28 @@
"name": "Galaxy", "name": "Galaxy",
"short_name": "Galaxy", "short_name": "Galaxy",
"description": "Galaxy — a turn-based space strategy game.", "description": "Galaxy — a turn-based space strategy game.",
"id": "/", "id": "./",
"start_url": "/", "start_url": "./",
"scope": "/", "scope": "./",
"display": "standalone", "display": "standalone",
"orientation": "any", "orientation": "any",
"background_color": "#0a0e1a", "background_color": "#0a0e1a",
"theme_color": "#0a0e1a", "theme_color": "#0a0e1a",
"icons": [ "icons": [
{ {
"src": "/icons/icon-192.png", "src": "icons/icon-192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png", "type": "image/png",
"purpose": "any" "purpose": "any"
}, },
{ {
"src": "/icons/icon-512.png", "src": "icons/icon-512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png", "type": "image/png",
"purpose": "any" "purpose": "any"
}, },
{ {
"src": "/icons/icon-maskable-512.png", "src": "icons/icon-maskable-512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png", "type": "image/png",
"purpose": "maskable" "purpose": "maskable"
+7
View File
@@ -11,6 +11,13 @@ export default {
fallback: "index.html", fallback: "index.html",
strict: true, strict: true,
}), }),
paths: {
// Base path the app is served under. Empty by default so local
// dev, vitest, and Playwright run at the root unchanged; the
// deployed single-origin build sets BASE_PATH=/game and the
// edge Caddy serves the SPA under that prefix.
base: process.env.BASE_PATH ?? "",
},
serviceWorker: { serviceWorker: {
// Registered manually in the root layout for production only. // Registered manually in the root layout for production only.
// SvelteKit's auto-registration also runs under `vite dev`, where // SvelteKit's auto-registration also runs under `vite dev`, where
+4 -4
View File
@@ -8,13 +8,13 @@
// server picks up via `VITE_GATEWAY_RESPONSE_PUBLIC_KEY`. // server picks up via `VITE_GATEWAY_RESPONSE_PUBLIC_KEY`.
// //
// The Connect-Web request URL pattern is // The Connect-Web request URL pattern is
// <baseUrl>/galaxy.gateway.v1.EdgeGateway/<MethodName> // <baseUrl>/edge.v1.Gateway/<MethodName>
// so the route handlers below match against the trailing path // so the route handlers below match against the trailing path
// suffix and ignore the host. // suffix and ignore the host.
import { fromJson, type JsonValue } from "@bufbuild/protobuf"; import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response"; import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
import { import {
buildAccountResponsePayload, buildAccountResponsePayload,
@@ -54,7 +54,7 @@ async function mockGatewayHappyPath(
); );
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -105,7 +105,7 @@ async function mockGatewayHappyPath(
); );
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async (route) => { async (route) => {
// Hold the stream open until the test releases it via // Hold the stream open until the test releases it via
// `pendingSubscribes`. Releasing fulfils with a Connect // `pendingSubscribes`. Releasing fulfils with a Connect
+2 -2
View File
@@ -15,7 +15,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report"; import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { GameBattleRequest } from "../../src/proto/galaxy/fbs/battle"; import { GameBattleRequest } from "../../src/proto/galaxy/fbs/battle";
@@ -91,7 +91,7 @@ async function mockGatewayAndBattle(page: Page): Promise<void> {
}; };
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
+3 -3
View File
@@ -12,7 +12,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { import {
CommandPlanetRouteRemove, CommandPlanetRouteRemove,
@@ -110,7 +110,7 @@ async function mockGateway(page: Page): Promise<MockHandle> {
let submitCount = 0; let submitCount = 0;
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -267,7 +267,7 @@ async function mockGateway(page: Page): Promise<MockHandle> {
); );
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async () => { async () => {
await new Promise<void>(() => {}); await new Promise<void>(() => {});
}, },
+1 -1
View File
@@ -18,7 +18,7 @@
import { create, toJsonString } from "@bufbuild/protobuf"; import { create, toJsonString } from "@bufbuild/protobuf";
import { webcrypto } from "node:crypto"; import { webcrypto } from "node:crypto";
import { GatewayEventSchema } from "../../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { GatewayEventSchema } from "../../../src/proto/edge/v1/edge_gateway_pb";
import { buildEventSigningInput } from "./canon"; import { buildEventSigningInput } from "./canon";
import { import {
FIXTURE_PRIVATE_KEY_PKCS8_BASE64, FIXTURE_PRIVATE_KEY_PKCS8_BASE64,
@@ -8,7 +8,7 @@
import { create, toJson, toJsonString } from "@bufbuild/protobuf"; import { create, toJson, toJsonString } from "@bufbuild/protobuf";
import { webcrypto } from "node:crypto"; import { webcrypto } from "node:crypto";
import { ExecuteCommandResponseSchema } from "../../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandResponseSchema } from "../../../src/proto/edge/v1/edge_gateway_pb";
import { import {
FIXTURE_PRIVATE_KEY_PKCS8_BASE64, FIXTURE_PRIVATE_KEY_PKCS8_BASE64,
decodeBase64, decodeBase64,
@@ -10,7 +10,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report"; import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response"; import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
@@ -46,7 +46,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<void> {
}; };
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -93,7 +93,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<void> {
); );
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async () => { async () => {
await new Promise<void>(() => {}); await new Promise<void>(() => {});
}, },
+3 -3
View File
@@ -11,7 +11,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report"; import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response"; import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
@@ -53,7 +53,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockState> {
}; };
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -112,7 +112,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockState> {
// the watcher's catch path logs the abort and returns without a // the watcher's catch path logs the abort and returns without a
// sign-out — same convention as `tests/e2e/lobby-flow.spec.ts`. // sign-out — same convention as `tests/e2e/lobby-flow.spec.ts`.
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async () => { async () => {
await new Promise<void>(() => {}); await new Promise<void>(() => {});
}, },
+3 -3
View File
@@ -30,7 +30,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report"; import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response"; import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
import { import {
@@ -70,7 +70,7 @@ async function mockGateway(page: Page): Promise<MockState> {
}); });
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -147,7 +147,7 @@ async function mockGateway(page: Page): Promise<MockState> {
); );
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async () => { async () => {
await new Promise<void>(() => {}); await new Promise<void>(() => {});
}, },
+3 -3
View File
@@ -12,7 +12,7 @@
import { fromJson, type JsonValue } from "@bufbuild/protobuf"; import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { GameCreateRequest } from "../../src/proto/galaxy/fbs/lobby"; import { GameCreateRequest } from "../../src/proto/galaxy/fbs/lobby";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response"; import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
import { import {
@@ -74,7 +74,7 @@ async function mockGateway(page: Page, initial: Partial<LobbyState> = {}): Promi
}); });
}); });
await page.route("**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", async (route) => { await page.route("**/edge.v1.Gateway/ExecuteCommand", async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
await route.fulfill({ status: 400 }); await route.fulfill({ status: 400 });
@@ -208,7 +208,7 @@ async function mockGateway(page: Page, initial: Partial<LobbyState> = {}): Promi
}); });
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async (route) => { async (route) => {
const action = await new Promise<"endOfStream" | "abort">((resolve) => { const action = await new Promise<"endOfStream" | "abort">((resolve) => {
mocks.pendingSubscribes.push(() => resolve("endOfStream")); mocks.pendingSubscribes.push(() => resolve("endOfStream"));
+3 -3
View File
@@ -10,7 +10,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { UserGamesOrderGet } from "../../src/proto/galaxy/fbs/order"; import { UserGamesOrderGet } from "../../src/proto/galaxy/fbs/order";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report"; import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
@@ -44,7 +44,7 @@ async function mockGateway(page: Page): Promise<void> {
}; };
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -116,7 +116,7 @@ async function mockGateway(page: Page): Promise<void> {
); );
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async () => { async () => {
await new Promise<void>(() => {}); await new Promise<void>(() => {});
}, },
+3 -3
View File
@@ -20,7 +20,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report"; import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response"; import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
@@ -57,7 +57,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<void> {
}; };
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -183,7 +183,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<void> {
// sign the session out mid-test (same convention as // sign the session out mid-test (same convention as
// `game-shell-map.spec.ts`). // `game-shell-map.spec.ts`).
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async () => { async () => {
await new Promise<void>(() => {}); await new Promise<void>(() => {});
}, },
+3 -3
View File
@@ -9,7 +9,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { import {
UserGamesOrder, UserGamesOrder,
@@ -76,7 +76,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
let submitCalls = 0; let submitCalls = 0;
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -204,7 +204,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
let subscribeServed = false; let subscribeServed = false;
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async (route) => { async (route) => {
if (opts.subscribeFrame !== undefined && !subscribeServed) { if (opts.subscribeFrame !== undefined && !subscribeServed) {
subscribeServed = true; subscribeServed = true;
@@ -12,7 +12,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { import {
CommandPlanetProduce, CommandPlanetProduce,
@@ -74,7 +74,7 @@ async function mockGateway(page: Page): Promise<MockHandle> {
let submitCount = 0; let submitCount = 0;
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -187,7 +187,7 @@ async function mockGateway(page: Page): Promise<MockHandle> {
); );
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async () => { async () => {
await new Promise<void>(() => {}); await new Promise<void>(() => {});
}, },
+3 -3
View File
@@ -14,7 +14,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { import {
CommandPayload, CommandPayload,
@@ -65,7 +65,7 @@ async function mockGateway(page: Page): Promise<MockHandle> {
let lastVote: MockHandle["lastVote"] = null; let lastVote: MockHandle["lastVote"] = null;
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -239,7 +239,7 @@ async function mockGateway(page: Page): Promise<MockHandle> {
); );
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async () => { async () => {
await new Promise<void>(() => {}); await new Promise<void>(() => {});
}, },
+3 -3
View File
@@ -11,7 +11,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { import {
UserGamesOrder, UserGamesOrder,
@@ -65,7 +65,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
let lastReportName = "Earth"; let lastReportName = "Earth";
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -181,7 +181,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
); );
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async () => { async () => {
await new Promise<void>(() => {}); await new Promise<void>(() => {});
}, },
@@ -16,7 +16,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report"; import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response"; import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
@@ -92,7 +92,7 @@ async function mockGateway(page: Page): Promise<void> {
const storedOrder: CommandResultFixture[] = []; const storedOrder: CommandResultFixture[] = [];
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -195,7 +195,7 @@ async function mockGateway(page: Page): Promise<void> {
); );
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async () => { async () => {
await new Promise<void>(() => {}); await new Promise<void>(() => {});
}, },
@@ -9,7 +9,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { import {
CommandPayload, CommandPayload,
@@ -51,7 +51,7 @@ async function mockGateway(page: Page): Promise<void> {
let storedOrder: CommandResultFixture[] = []; let storedOrder: CommandResultFixture[] = [];
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -155,7 +155,7 @@ async function mockGateway(page: Page): Promise<void> {
); );
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async () => { async () => {
await new Promise<void>(() => {}); await new Promise<void>(() => {});
}, },
+3 -3
View File
@@ -22,7 +22,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { import {
CommandPayload, CommandPayload,
@@ -94,7 +94,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
const reportSciences: ScienceFixture[] = [...(opts.initialSciences ?? [])]; const reportSciences: ScienceFixture[] = [...(opts.initialSciences ?? [])];
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -242,7 +242,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
); );
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async () => { async () => {
await new Promise<void>(() => {}); await new Promise<void>(() => {});
}, },
+3 -3
View File
@@ -20,7 +20,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { import {
CommandPayload, CommandPayload,
@@ -88,7 +88,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
const reportClasses: ShipClassFixture[] = [...(opts.initialClasses ?? [])]; const reportClasses: ShipClassFixture[] = [...(opts.initialClasses ?? [])];
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -220,7 +220,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
); );
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async () => { async () => {
await new Promise<void>(() => {}); await new Promise<void>(() => {});
}, },
+3 -3
View File
@@ -10,7 +10,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test"; import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers"; import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common"; import { UUID } from "../../src/proto/galaxy/fbs/common";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report"; import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response"; import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
@@ -52,7 +52,7 @@ async function mockGateway(page: Page): Promise<MockState> {
}); });
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", "**/edge.v1.Gateway/ExecuteCommand",
async (route) => { async (route) => {
const reqText = route.request().postData(); const reqText = route.request().postData();
if (reqText === null) { if (reqText === null) {
@@ -111,7 +111,7 @@ async function mockGateway(page: Page): Promise<MockState> {
// end-of-body) are held open indefinitely so the toast stays // end-of-body) are held open indefinitely so the toast stays
// visible long enough for the test to interact with it. // visible long enough for the test to interact with it.
await page.route( await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents", "**/edge.v1.Gateway/SubscribeEvents",
async (route) => { async (route) => {
state.subscribeHits += 1; state.subscribeHits += 1;
if (state.subscribeHits === 1) { if (state.subscribeHits === 1) {
+5 -5
View File
@@ -16,10 +16,10 @@ import {
createRouterTransport, createRouterTransport,
} from "@connectrpc/connect"; } from "@connectrpc/connect";
import { import {
EdgeGateway, Gateway,
GatewayEventSchema, GatewayEventSchema,
type GatewayEvent, type GatewayEvent,
} from "../src/proto/galaxy/gateway/v1/edge_gateway_pb"; } from "../src/proto/edge/v1/edge_gateway_pb";
let sessionStatus: "anonymous" | "authenticated" = "anonymous"; let sessionStatus: "anonymous" | "authenticated" = "anonymous";
const signOutSpy = vi.fn(); const signOutSpy = vi.fn();
@@ -91,9 +91,9 @@ function buildEvent(eventType: string, payload: Uint8Array): GatewayEvent {
function makeRouter( function makeRouter(
streamFactory: () => AsyncIterable<GatewayEvent>, streamFactory: () => AsyncIterable<GatewayEvent>,
): ReturnType<typeof createClient<typeof EdgeGateway>> { ): ReturnType<typeof createClient<typeof Gateway>> {
const transport = createRouterTransport(({ service }) => { const transport = createRouterTransport(({ service }) => {
service(EdgeGateway, { service(Gateway, {
executeCommand() { executeCommand() {
throw new Error("not used in this test"); throw new Error("not used in this test");
}, },
@@ -104,7 +104,7 @@ function makeRouter(
}, },
}); });
}); });
return createClient(EdgeGateway, transport); return createClient(Gateway, transport);
} }
describe("EventStream", () => { describe("EventStream", () => {

Some files were not shown because too many files have changed in this diff Show More