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

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