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