feat(deploy): single-origin path-based deployment + project site
Build · Site / build (push) Successful in 8s
Tests · Go / test (push) Successful in 2m22s
Tests · UI / test (push) Failing after 2m42s

Serve the whole stack behind one host: site at /, game UI at /game/,
gateway REST at /api + /healthz, Connect at /rpc (prefix stripped by the
edge Caddy). The built artifact is domain-agnostic — the UI talks to the
gateway same-origin via relative URLs, so the same bundle runs under any
host with no rebuild and with CORS disabled.

- Rename the Connect proto service galaxy.gateway.v1.EdgeGateway ->
  edge.v1.Gateway; regenerate Go + TS; public path /rpc/edge.v1.Gateway.
- Move the game UI under base path /game (env BASE_PATH); make the
  manifest, service-worker scope, WASM loader, and all navigation
  base-aware via a withBase helper.
- Relative API + /rpc Connect prefix; Vite dev proxy mirrors the strip.
- Rewrite the edge Caddy (dev + prod) for path-based routing; empty CORS
  allow-lists (same-origin); single host.
- New VitePress project site (site/): i18n en/ru with switcher, LaTeX
  math, minimal monospace theme; built and served at /.
- dev-deploy compose/Makefile + CI (dev-deploy, prod-build, new
  site-build) build and seed the site; probes hit /, /game/, /healthz.
- Sync docs (ARCHITECTURE, gateway README/openapi, dev-deploy &
  local-dev READMEs, CLAUDE.md, ui/PLAN).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-23 18:19:07 +02:00
parent fa0df5183a
commit 8565942392
104 changed files with 2967 additions and 787 deletions
+6 -6
View File
@@ -474,11 +474,11 @@ documents the historical labelling in its package doc.
Artifacts (delivered):
- `gateway/buf.gen.yaml` extended with `buf.build/connectrpc/go`,
generating `gateway/proto/galaxy/gateway/v1/gatewayv1connect/edge_gateway.connect.go`
generating `gateway/proto/edge/v1/edgev1connect/edge_gateway.connect.go`
- `gateway/internal/grpcapi/server.go` rewritten around `http.Server`
+ `h2c.NewHandler` + `gatewayv1connect.NewEdgeGatewayHandler`
+ `h2c.NewHandler` + `edgev1connect.NewGatewayHandler`
- new `gateway/internal/grpcapi/connect_handler.go` adapting the
existing `gatewayv1.EdgeGatewayServer` decorator stack to the
existing `edgev1.GatewayServer` decorator stack to the
Connect handler interface, including a `grpc.ServerStreamingServer`
shim around `*connect.ServerStream[GatewayEvent]` and a gRPC
`status.Error` → `*connect.Error` translation helper
@@ -492,7 +492,7 @@ Artifacts (delivered):
`gateway/docs/runbook.md`, and `docs/ARCHITECTURE.md` §15
- migrated tests: `gateway/internal/grpcapi/server_test.go`,
`test_fixtures_test.go`, and every `*_integration_test.go` in that
package now drive a `gatewayv1connect.EdgeGatewayClient` over
package now drive a `edgev1connect.GatewayClient` over
HTTP/2 cleartext loopback
- migrated harness: `integration/testenv/grpc_client.go` →
`connect_client.go`. `SignedGatewayClient` keeps the same public
@@ -580,10 +580,10 @@ Artifacts (delivered):
browsers; the JSDOM test path lives next to it in
`ui/frontend/tests/setup-wasm.ts`.
- `ui/frontend/src/api/connect.ts` — typed Connect-Web transport +
`EdgeGatewayClient` factory.
`GatewayClient` factory.
- `ui/frontend/src/api/galaxy-client.ts` — `GalaxyClient` skeleton
with injected `Signer` and `Sha256` dependencies.
- `ui/frontend/src/proto/galaxy/gateway/v1/edge_gateway_pb.ts`
- `ui/frontend/src/proto/edge/v1/edge_gateway_pb.ts`
(generated) and `ui/frontend/src/proto/buf/validate/validate_pb.ts`
(generated as a transitive import via `--include-imports`).
- `ui/frontend/static/core.wasm` (903 KB) + `wasm_exec.js` (TinyGo
+1 -1
View File
@@ -1,6 +1,6 @@
// Package types defines the v1 transport envelopes carried over the
// wire between the Galaxy client and gateway. Envelope shapes mirror
// the protobuf messages in `gateway/proto/galaxy/gateway/v1/`, but are
// the protobuf messages in `gateway/proto/edge/v1/`, but are
// kept as plain Go structs here so the UI client can read and produce
// them without depending on the protobuf runtime in WASM and gomobile
// builds.
+5 -4
View File
@@ -5,10 +5,11 @@
# Gateway public REST + Connect-Web edge listener. Points at the Vite
# dev server's own origin so the browser sees same-origin requests;
# Vite then proxies `/api` and `/galaxy.gateway.v1.EdgeGateway` to the
# real gateway at `http://localhost:8080`. See `vite.config.ts`. To
# work against a non-local gateway, override the proxy target via
# `VITE_DEV_PROXY_TARGET=http://gateway.host:8080 pnpm dev` (no UI
# Vite then proxies `/api` to the REST listener (`:8080`) and `/rpc` to
# the Connect listener (`:9090`, prefix stripped), mirroring the
# single-origin edge Caddy. See `vite.config.ts`. To work against a
# non-local gateway, override the proxy targets via
# `VITE_DEV_PROXY_TARGET` / `VITE_DEV_GRPC_PROXY_TARGET` (no UI
# rebuild needed).
VITE_GATEWAY_BASE_URL=http://localhost:5173
+9 -8
View File
@@ -1,20 +1,21 @@
// `createEdgeGatewayClient` builds a typed Connect-Web client for the
// `createGatewayClient` builds a typed Connect-Web client for the
// gateway's authenticated edge surface. It speaks the Connect protocol
// over HTTP/1.1 (or HTTP/2 if the host upgrades the connection) — the
// gateway listener built in Phase 4 natively serves Connect, gRPC, and
// gRPC-Web on the same h2c port.
//
// The factory is intentionally thin: callers provide the gateway base
// URL (e.g. https://api.galaxy.test), and receive a typed
// `EdgeGatewayClient`. Authentication, signing, and response
// The factory is intentionally thin: callers provide the Connect base
// URL (the same-origin `/rpc` prefix from `gatewayRpcBaseUrl`), and
// receive a typed
// `GatewayClient`. Authentication, signing, and response
// verification live one layer up, in `GalaxyClient`.
import { createClient, type Client } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { EdgeGateway } from "../proto/galaxy/gateway/v1/edge_gateway_pb";
import { Gateway } from "../proto/edge/v1/edge_gateway_pb";
export type EdgeGatewayClient = Client<typeof EdgeGateway>;
export type GatewayClient = Client<typeof Gateway>;
export function createEdgeGatewayClient(baseUrl: string): EdgeGatewayClient {
return createClient(EdgeGateway, createConnectTransport({ baseUrl }));
export function createGatewayClient(baseUrl: string): GatewayClient {
return createClient(Gateway, createConnectTransport({ baseUrl }));
}
+8 -8
View File
@@ -26,10 +26,10 @@ import type { DeviceKeypair } from "../platform/store/index";
import {
SubscribeEventsRequestSchema,
type GatewayEvent,
} from "../proto/galaxy/gateway/v1/edge_gateway_pb";
import { GATEWAY_BASE_URL } from "../lib/env";
} from "../proto/edge/v1/edge_gateway_pb";
import { gatewayRpcBaseUrl } from "../lib/env";
import { session } from "../lib/session-store.svelte";
import { createEdgeGatewayClient, type EdgeGatewayClient } from "./connect";
import { createGatewayClient, type GatewayClient } from "./connect";
const PROTOCOL_VERSION = "v1";
const SUBSCRIBE_MESSAGE_TYPE = "gateway.subscribe";
@@ -83,7 +83,7 @@ export type ConnectionStatus =
* consumer cannot resolve by itself. Production code reads `core`,
* `keypair`, and `deviceSessionId` from the session store and the
* gateway public key from `lib/env`; tests inject a fake
* `EdgeGatewayClient` and deterministic `sleep`/`random` to drive
* `GatewayClient` and deterministic `sleep`/`random` to drive
* backoff in fake-timer mode.
*/
export interface EventStreamStartOptions {
@@ -91,8 +91,8 @@ export interface EventStreamStartOptions {
keypair: DeviceKeypair;
deviceSessionId: string;
gatewayResponsePublicKey: Uint8Array;
/** Custom transport client. Defaults to `createEdgeGatewayClient(GATEWAY_BASE_URL)`. */
client?: EdgeGatewayClient;
/** Custom transport client. Defaults to `createGatewayClient(gatewayRpcBaseUrl())`. */
client?: GatewayClient;
/** Sleep hook for tests; defaults to a real-time `setTimeout`. */
sleep?: (ms: number) => Promise<void>;
/** Random source for full-jitter backoff; defaults to `Math.random`. */
@@ -189,7 +189,7 @@ export class EventStream {
const sleep = opts.sleep ?? defaultSleep;
const random = opts.random ?? Math.random;
const onlineProbe = opts.onlineProbe ?? defaultOnlineProbe;
const client = opts.client ?? createEdgeGatewayClient(GATEWAY_BASE_URL);
const client = opts.client ?? createGatewayClient(gatewayRpcBaseUrl());
let attempt = 0;
while (!signal.aborted && this.running) {
@@ -311,7 +311,7 @@ export class EventStream {
}
async function openStream(
client: EdgeGatewayClient,
client: GatewayClient,
opts: EventStreamStartOptions,
signal: AbortSignal,
): Promise<AsyncIterable<GatewayEvent>> {
+4 -4
View File
@@ -15,8 +15,8 @@ import type { Core } from "../platform/core/index";
import {
ExecuteCommandRequestSchema,
type ExecuteCommandResponse,
} from "../proto/galaxy/gateway/v1/edge_gateway_pb";
import type { EdgeGatewayClient } from "./connect";
} from "../proto/edge/v1/edge_gateway_pb";
import type { GatewayClient } from "./connect";
/**
* Signer produces a raw 64-byte Ed25519 signature over canonicalBytes.
@@ -35,7 +35,7 @@ export type Sha256 = (payload: Uint8Array) => Promise<Uint8Array>;
export interface GalaxyClientOptions {
core: Core;
edge: EdgeGatewayClient;
edge: GatewayClient;
signer: Signer;
sha256: Sha256;
deviceSessionId: string;
@@ -53,7 +53,7 @@ export interface ExecuteCommandResult {
export class GalaxyClient {
private readonly core: Core;
private readonly edge: EdgeGatewayClient;
private readonly edge: GatewayClient;
private readonly signer: Signer;
private readonly sha256: Sha256;
private readonly deviceSessionId: string;
@@ -12,6 +12,7 @@ header now — we just hand the routes down as callbacks so the
viewer keeps its prop-driven contract.
-->
<script lang="ts">
import { withBase } from "$lib/paths";
import { getContext } from "svelte";
import { goto } from "$app/navigation";
@@ -126,10 +127,10 @@ viewer keeps its prop-driven contract.
});
function backToReport() {
goto(`/games/${gameId}/report`);
goto(withBase(`/games/${gameId}/report`));
}
function backToMap() {
goto(`/games/${gameId}/map`);
goto(withBase(`/games/${gameId}/map`));
}
</script>
@@ -25,6 +25,7 @@ fractions is a Phase 21 decision documented in
`ui/docs/science-designer-ux.md`.
-->
<script lang="ts">
import { withBase } from "$lib/paths";
import { getContext, tick } from "svelte";
import { goto } from "$app/navigation";
import { page } from "$app/state";
@@ -125,7 +126,7 @@ fractions is a Phase 21 decision documented in
}
function backToTable(): void {
void goto(`/games/${gameId}/table/sciences`);
void goto(withBase(`/games/${gameId}/table/sciences`));
}
async function save(): Promise<void> {
+3 -2
View File
@@ -20,6 +20,7 @@ Phase 29 wires the wrap-mode toggle on top of the per-game `wrapMode`
preference the store already manages.
-->
<script lang="ts">
import { withBase } from "$lib/paths";
import { getContext, onDestroy, onMount, untrack } from "svelte";
import { goto } from "$app/navigation";
import { page } from "$app/state";
@@ -636,14 +637,14 @@ preference the store already manages.
const gameId = page.params.id ?? "";
const turn = store?.report?.turn ?? 0;
void goto(
`/games/${gameId}/battle/${target.battleId}?turn=${turn}`,
withBase(`/games/${gameId}/battle/${target.battleId}?turn=${turn}`),
);
break;
}
case "bombing": {
const gameId = page.params.id ?? "";
void goto(
`/games/${gameId}/report#report-bombings`,
withBase(`/games/${gameId}/report#report-bombings`),
).then(() => {
if (typeof document === "undefined") return;
const row = document.querySelector(
@@ -20,6 +20,7 @@ The active section is computed by the orchestrator
`activeSlug` prop. The TOC itself owns no observers.
-->
<script lang="ts">
import { withBase } from "$lib/paths";
import { goto } from "$app/navigation";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
@@ -62,7 +63,7 @@ The active section is computed by the orchestrator
}
async function backToMap(): Promise<void> {
await goto(`/games/${gameId}/map`);
await goto(withBase(`/games/${gameId}/map`));
}
</script>
@@ -7,6 +7,7 @@ monospace `<span>`; the rewire here is the one-liner the Phase 23
decision log called out.
-->
<script lang="ts">
import { withBase } from "$lib/paths";
import { getContext } from "svelte";
import { page } from "$app/state";
@@ -47,7 +48,7 @@ decision log called out.
</span>
<a
class="uuid"
href={`/games/${gameId}/battle/${b.id}?turn=${turn}`}
href={withBase(`/games/${gameId}/battle/${b.id}?turn=${turn}`)}
data-testid="report-battle-row"
data-id={b.id}
>{b.id}</a>
@@ -17,6 +17,7 @@ The component sits inside the active-view slot owned by
data fetching is performed here — the layout is responsible.
-->
<script lang="ts">
import { withBase } from "$lib/paths";
import { getContext } from "svelte";
import { goto } from "$app/navigation";
import { page } from "$app/state";
@@ -117,11 +118,11 @@ data fetching is performed here — the layout is responsible.
}
function openDesigner(name: string): void {
void goto(`/games/${gameId}/designer/science/${encodeURIComponent(name)}`);
void goto(withBase(`/games/${gameId}/designer/science/${encodeURIComponent(name)}`));
}
function newScience(): void {
void goto(`/games/${gameId}/designer/science`);
void goto(withBase(`/games/${gameId}/designer/science`));
}
async function deleteScience(name: string): Promise<void> {
+23 -3
View File
@@ -3,9 +3,14 @@
// at the first import.
//
// `VITE_GATEWAY_BASE_URL` is the base URL of the gateway public REST
// surface and the Connect-Web authenticated edge (same host, same
// port; the gateway listener serves both). It defaults to the local
// dev address used by `tools/local-ci` and the integration suite.
// surface. An empty value means "same origin": the single-origin
// deployment serves the UI, the REST surface (`/api/...`), and the
// authenticated Connect-Web edge (`/rpc/...`) behind one host, so the
// browser issues same-origin requests and needs no absolute base. A
// non-empty value (the Vite dev proxy, `tools/local-ci`, the
// integration suite) points REST at that absolute host instead. The
// Connect base is derived from this value by `gatewayRpcBaseUrl`,
// which appends the `/rpc` routing prefix.
//
// `VITE_GATEWAY_RESPONSE_PUBLIC_KEY` is the gateway's response-signing
// Ed25519 public key, encoded as standard (non-URL-safe) base64 of
@@ -26,6 +31,21 @@ const RAW_RESPONSE_PUBLIC_KEY: string =
export const GATEWAY_BASE_URL: string = stripTrailingSlash(RAW_BASE_URL);
/**
* gatewayRpcBaseUrl is the base URL for the authenticated Connect-Web
* surface. The edge Caddy and the Vite dev proxy route the `/rpc`
* prefix to the gateway's Connect listener, stripping it before the
* request reaches the proto-derived `edge.v1.Gateway` service path.
* When `GATEWAY_BASE_URL` is empty the gateway shares the document
* origin, so the origin is resolved at call time from `window`.
*/
export function gatewayRpcBaseUrl(): string {
const origin =
GATEWAY_BASE_URL ||
(typeof window !== "undefined" ? window.location.origin : "");
return `${origin}/rpc`;
}
export const GATEWAY_RESPONSE_PUBLIC_KEY: Uint8Array = decodeBase64(
RAW_RESPONSE_PUBLIC_KEY,
);
+2 -1
View File
@@ -12,6 +12,7 @@ navigation. Phase 26 introduces the history-mode entry; Phase 35
polishes microcopy.
-->
<script lang="ts">
import { withBase } from "$lib/paths";
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
@@ -41,7 +42,7 @@ polishes microcopy.
function go(path: string): void {
open = false;
void goto(path);
void goto(withBase(path));
}
function onKeyDown(event: KeyboardEvent): void {
+25
View File
@@ -0,0 +1,25 @@
// Base-path helpers for the single-origin deployment. The game UI is
// served under `kit.paths.base` — empty at the root for local dev,
// vitest, and Playwright, and `/game` in the deployed single-origin
// build. SvelteKit does not auto-prefix `goto`, `<a href>`, raw asset
// fetches, or the service-worker scope, so every app-internal absolute
// path is routed through `withBase`.
//
// `base` from `$app/paths` is the low-level primitive that `resolve()`
// builds on. We use it directly here (rather than `resolve()`) because
// the client navigates to and fetches dynamic, runtime-built paths
// (command routes, `core.wasm`, the service worker) that `resolve()`'s
// statically-typed route-id surface cannot express.
import { base } from "$app/paths";
/** appBase is the configured base path (empty string at the root). */
export const appBase = base;
/**
* withBase prefixes an app-internal absolute path (leading slash) with
* the configured base path. At the root it returns the path unchanged;
* under the single-origin deployment it yields e.g. `/game/lobby`.
*/
export function withBase(path: string): string {
return `${base}${path}`;
}
@@ -13,6 +13,7 @@ exists; until then the convenience of one source of truth for
destinations beats the duplication.
-->
<script lang="ts">
import { withBase } from "$lib/paths";
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
@@ -47,13 +48,13 @@ destinations beats the duplication.
async function selectTool(tool: MobileTool): Promise<void> {
moreOpen = false;
onSelectTool(tool);
await goto(`/games/${gameId}/map`);
await goto(withBase(`/games/${gameId}/map`));
}
async function go(path: string): Promise<void> {
moreOpen = false;
onSelectTool("map");
await goto(path);
await goto(withBase(path));
}
function toggleMore(): void {
+3 -2
View File
@@ -34,6 +34,7 @@ import type {
WeaponsBlockInput,
WeaponsForAttackInput,
} from "./index";
import { withBase } from "$lib/paths";
/**
* GalaxyCoreBridge is the shape Go installs on `globalThis.galaxyCore`.
@@ -143,7 +144,7 @@ async function bootBrowserWasm(): Promise<Core> {
throw new Error("loadWasmCore: Go runtime missing after wasm_exec.js load");
}
const go = new Go();
const response = await fetch("/core.wasm");
const response = await fetch(withBase("/core.wasm"));
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes, go.importObject);
void go.run(instance);
@@ -156,7 +157,7 @@ async function ensureGoRuntimeLoaded(): Promise<void> {
}
await new Promise<void>((resolve, reject) => {
const script = document.createElement("script");
script.src = "/wasm_exec.js";
script.src = withBase("/wasm_exec.js");
script.onload = () => resolve();
script.onerror = () => reject(new Error("failed to load /wasm_exec.js"));
document.head.appendChild(script);
@@ -1,22 +1,22 @@
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
// @generated from file galaxy/gateway/v1/edge_gateway.proto (package galaxy.gateway.v1, syntax proto3)
// @generated from file edge/v1/edge_gateway.proto (package edge.v1, syntax proto3)
/* eslint-disable */
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import { file_buf_validate_validate } from "../../../buf/validate/validate_pb";
import { file_buf_validate_validate } from "../../buf/validate/validate_pb";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file galaxy/gateway/v1/edge_gateway.proto.
* Describes the file edge/v1/edge_gateway.proto.
*/
export const file_galaxy_gateway_v1_edge_gateway: GenFile = /*@__PURE__*/
fileDesc("CiRnYWxheHkvZ2F0ZXdheS92MS9lZGdlX2dhdGV3YXkucHJvdG8SEWdhbGF4eS5nYXRld2F5LnYxIqYCChVFeGVjdXRlQ29tbWFuZFJlcXVlc3QSIQoQcHJvdG9jb2xfdmVyc2lvbhgBIAEoCUIHukgEcgIQARIiChFkZXZpY2Vfc2Vzc2lvbl9pZBgCIAEoCUIHukgEcgIQARIdCgxtZXNzYWdlX3R5cGUYAyABKAlCB7pIBHICEAESHQoMdGltZXN0YW1wX21zGAQgASgDQge6SAQiAiAAEhsKCnJlcXVlc3RfaWQYBSABKAlCB7pIBHICEAESHgoNcGF5bG9hZF9ieXRlcxgGIAEoDEIHukgEegIQARIdCgxwYXlsb2FkX2hhc2gYByABKAxCB7pIBHoCEAESGgoJc2lnbmF0dXJlGAggASgMQge6SAR6AhABEhAKCHRyYWNlX2lkGAkgASgJIrEBChZFeGVjdXRlQ29tbWFuZFJlc3BvbnNlEhgKEHByb3RvY29sX3ZlcnNpb24YASABKAkSEgoKcmVxdWVzdF9pZBgCIAEoCRIUCgx0aW1lc3RhbXBfbXMYAyABKAMSEwoLcmVzdWx0X2NvZGUYBCABKAkSFQoNcGF5bG9hZF9ieXRlcxgFIAEoDBIUCgxwYXlsb2FkX2hhc2gYBiABKAwSEQoJc2lnbmF0dXJlGAcgASgMIp4CChZTdWJzY3JpYmVFdmVudHNSZXF1ZXN0EiEKEHByb3RvY29sX3ZlcnNpb24YASABKAlCB7pIBHICEAESIgoRZGV2aWNlX3Nlc3Npb25faWQYAiABKAlCB7pIBHICEAESHQoMbWVzc2FnZV90eXBlGAMgASgJQge6SARyAhABEh0KDHRpbWVzdGFtcF9tcxgEIAEoA0IHukgEIgIgABIbCgpyZXF1ZXN0X2lkGAUgASgJQge6SARyAhABEh0KDHBheWxvYWRfaGFzaBgGIAEoDEIHukgEegIQARIaCglzaWduYXR1cmUYByABKAxCB7pIBHoCEAESFQoNcGF5bG9hZF9ieXRlcxgIIAEoDBIQCgh0cmFjZV9pZBgJIAEoCSKwAQoMR2F0ZXdheUV2ZW50EhIKCmV2ZW50X3R5cGUYASABKAkSEAoIZXZlbnRfaWQYAiABKAkSFAoMdGltZXN0YW1wX21zGAMgASgDEhUKDXBheWxvYWRfYnl0ZXMYBCABKAwSFAoMcGF5bG9hZF9oYXNoGAUgASgMEhEKCXNpZ25hdHVyZRgGIAEoDBISCgpyZXF1ZXN0X2lkGAcgASgJEhAKCHRyYWNlX2lkGAggASgJMtUBCgtFZGdlR2F0ZXdheRJlCg5FeGVjdXRlQ29tbWFuZBIoLmdhbGF4eS5nYXRld2F5LnYxLkV4ZWN1dGVDb21tYW5kUmVxdWVzdBopLmdhbGF4eS5nYXRld2F5LnYxLkV4ZWN1dGVDb21tYW5kUmVzcG9uc2USXwoPU3Vic2NyaWJlRXZlbnRzEikuZ2FsYXh5LmdhdGV3YXkudjEuU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBofLmdhbGF4eS5nYXRld2F5LnYxLkdhdGV3YXlFdmVudDABQjJaMGdhbGF4eS9nYXRld2F5L3Byb3RvL2dhbGF4eS9nYXRld2F5L3YxO2dhdGV3YXl2MWIGcHJvdG8z", [file_buf_validate_validate]);
export const file_edge_v1_edge_gateway: GenFile = /*@__PURE__*/
fileDesc("ChplZGdlL3YxL2VkZ2VfZ2F0ZXdheS5wcm90bxIHZWRnZS52MSKmAgoVRXhlY3V0ZUNvbW1hbmRSZXF1ZXN0EiEKEHByb3RvY29sX3ZlcnNpb24YASABKAlCB7pIBHICEAESIgoRZGV2aWNlX3Nlc3Npb25faWQYAiABKAlCB7pIBHICEAESHQoMbWVzc2FnZV90eXBlGAMgASgJQge6SARyAhABEh0KDHRpbWVzdGFtcF9tcxgEIAEoA0IHukgEIgIgABIbCgpyZXF1ZXN0X2lkGAUgASgJQge6SARyAhABEh4KDXBheWxvYWRfYnl0ZXMYBiABKAxCB7pIBHoCEAESHQoMcGF5bG9hZF9oYXNoGAcgASgMQge6SAR6AhABEhoKCXNpZ25hdHVyZRgIIAEoDEIHukgEegIQARIQCgh0cmFjZV9pZBgJIAEoCSKxAQoWRXhlY3V0ZUNvbW1hbmRSZXNwb25zZRIYChBwcm90b2NvbF92ZXJzaW9uGAEgASgJEhIKCnJlcXVlc3RfaWQYAiABKAkSFAoMdGltZXN0YW1wX21zGAMgASgDEhMKC3Jlc3VsdF9jb2RlGAQgASgJEhUKDXBheWxvYWRfYnl0ZXMYBSABKAwSFAoMcGF5bG9hZF9oYXNoGAYgASgMEhEKCXNpZ25hdHVyZRgHIAEoDCKeAgoWU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBIhChBwcm90b2NvbF92ZXJzaW9uGAEgASgJQge6SARyAhABEiIKEWRldmljZV9zZXNzaW9uX2lkGAIgASgJQge6SARyAhABEh0KDG1lc3NhZ2VfdHlwZRgDIAEoCUIHukgEcgIQARIdCgx0aW1lc3RhbXBfbXMYBCABKANCB7pIBCICIAASGwoKcmVxdWVzdF9pZBgFIAEoCUIHukgEcgIQARIdCgxwYXlsb2FkX2hhc2gYBiABKAxCB7pIBHoCEAESGgoJc2lnbmF0dXJlGAcgASgMQge6SAR6AhABEhUKDXBheWxvYWRfYnl0ZXMYCCABKAwSEAoIdHJhY2VfaWQYCSABKAkisAEKDEdhdGV3YXlFdmVudBISCgpldmVudF90eXBlGAEgASgJEhAKCGV2ZW50X2lkGAIgASgJEhQKDHRpbWVzdGFtcF9tcxgDIAEoAxIVCg1wYXlsb2FkX2J5dGVzGAQgASgMEhQKDHBheWxvYWRfaGFzaBgFIAEoDBIRCglzaWduYXR1cmUYBiABKAwSEgoKcmVxdWVzdF9pZBgHIAEoCRIQCgh0cmFjZV9pZBgIIAEoCTKpAQoHR2F0ZXdheRJRCg5FeGVjdXRlQ29tbWFuZBIeLmVkZ2UudjEuRXhlY3V0ZUNvbW1hbmRSZXF1ZXN0Gh8uZWRnZS52MS5FeGVjdXRlQ29tbWFuZFJlc3BvbnNlEksKD1N1YnNjcmliZUV2ZW50cxIfLmVkZ2UudjEuU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBoVLmVkZ2UudjEuR2F0ZXdheUV2ZW50MAFCJVojZ2FsYXh5L2dhdGV3YXkvcHJvdG8vZWRnZS92MTtlZGdldjFiBnByb3RvMw", [file_buf_validate_validate]);
/**
* @generated from message galaxy.gateway.v1.ExecuteCommandRequest
* @generated from message edge.v1.ExecuteCommandRequest
*/
export type ExecuteCommandRequest = Message<"galaxy.gateway.v1.ExecuteCommandRequest"> & {
export type ExecuteCommandRequest = Message<"edge.v1.ExecuteCommandRequest"> & {
/**
* protocol_version identifies the request envelope version. The gateway
* accepts only the literal "v1" after required-field validation succeeds.
@@ -69,16 +69,16 @@ export type ExecuteCommandRequest = Message<"galaxy.gateway.v1.ExecuteCommandReq
};
/**
* Describes the message galaxy.gateway.v1.ExecuteCommandRequest.
* Describes the message edge.v1.ExecuteCommandRequest.
* Use `create(ExecuteCommandRequestSchema)` to create a new message.
*/
export const ExecuteCommandRequestSchema: GenMessage<ExecuteCommandRequest> = /*@__PURE__*/
messageDesc(file_galaxy_gateway_v1_edge_gateway, 0);
messageDesc(file_edge_v1_edge_gateway, 0);
/**
* @generated from message galaxy.gateway.v1.ExecuteCommandResponse
* @generated from message edge.v1.ExecuteCommandResponse
*/
export type ExecuteCommandResponse = Message<"galaxy.gateway.v1.ExecuteCommandResponse"> & {
export type ExecuteCommandResponse = Message<"edge.v1.ExecuteCommandResponse"> & {
/**
* @generated from field: string protocol_version = 1;
*/
@@ -116,16 +116,16 @@ export type ExecuteCommandResponse = Message<"galaxy.gateway.v1.ExecuteCommandRe
};
/**
* Describes the message galaxy.gateway.v1.ExecuteCommandResponse.
* Describes the message edge.v1.ExecuteCommandResponse.
* Use `create(ExecuteCommandResponseSchema)` to create a new message.
*/
export const ExecuteCommandResponseSchema: GenMessage<ExecuteCommandResponse> = /*@__PURE__*/
messageDesc(file_galaxy_gateway_v1_edge_gateway, 1);
messageDesc(file_edge_v1_edge_gateway, 1);
/**
* @generated from message galaxy.gateway.v1.SubscribeEventsRequest
* @generated from message edge.v1.SubscribeEventsRequest
*/
export type SubscribeEventsRequest = Message<"galaxy.gateway.v1.SubscribeEventsRequest"> & {
export type SubscribeEventsRequest = Message<"edge.v1.SubscribeEventsRequest"> & {
/**
* protocol_version identifies the request envelope version. The gateway
* accepts only the literal "v1" after required-field validation succeeds.
@@ -179,16 +179,16 @@ export type SubscribeEventsRequest = Message<"galaxy.gateway.v1.SubscribeEventsR
};
/**
* Describes the message galaxy.gateway.v1.SubscribeEventsRequest.
* Describes the message edge.v1.SubscribeEventsRequest.
* Use `create(SubscribeEventsRequestSchema)` to create a new message.
*/
export const SubscribeEventsRequestSchema: GenMessage<SubscribeEventsRequest> = /*@__PURE__*/
messageDesc(file_galaxy_gateway_v1_edge_gateway, 2);
messageDesc(file_edge_v1_edge_gateway, 2);
/**
* @generated from message galaxy.gateway.v1.GatewayEvent
* @generated from message edge.v1.GatewayEvent
*/
export type GatewayEvent = Message<"galaxy.gateway.v1.GatewayEvent"> & {
export type GatewayEvent = Message<"edge.v1.GatewayEvent"> & {
/**
* @generated from field: string event_type = 1;
*/
@@ -231,18 +231,18 @@ export type GatewayEvent = Message<"galaxy.gateway.v1.GatewayEvent"> & {
};
/**
* Describes the message galaxy.gateway.v1.GatewayEvent.
* Describes the message edge.v1.GatewayEvent.
* Use `create(GatewayEventSchema)` to create a new message.
*/
export const GatewayEventSchema: GenMessage<GatewayEvent> = /*@__PURE__*/
messageDesc(file_galaxy_gateway_v1_edge_gateway, 3);
messageDesc(file_edge_v1_edge_gateway, 3);
/**
* @generated from service galaxy.gateway.v1.EdgeGateway
* @generated from service edge.v1.Gateway
*/
export const EdgeGateway: GenService<{
export const Gateway: GenService<{
/**
* @generated from rpc galaxy.gateway.v1.EdgeGateway.ExecuteCommand
* @generated from rpc edge.v1.Gateway.ExecuteCommand
*/
executeCommand: {
methodKind: "unary";
@@ -250,7 +250,7 @@ export const EdgeGateway: GenService<{
output: typeof ExecuteCommandResponseSchema;
},
/**
* @generated from rpc galaxy.gateway.v1.EdgeGateway.SubscribeEvents
* @generated from rpc edge.v1.Gateway.SubscribeEvents
*/
subscribeEvents: {
methodKind: "server_streaming";
@@ -258,5 +258,5 @@ export const EdgeGateway: GenService<{
output: typeof GatewayEventSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_galaxy_gateway_v1_edge_gateway, 0);
serviceDesc(file_edge_v1_edge_gateway, 0);
+9 -4
View File
@@ -5,6 +5,7 @@
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { dev } from "$app/environment";
import { appBase, withBase } from "$lib/paths";
import { i18n } from "$lib/i18n/index.svelte";
import { session } from "$lib/session-store.svelte";
import { eventStream } from "../api/events.svelte";
@@ -26,7 +27,9 @@
// in svelte.config.js) so `vite dev` and the dev-server e2e suite
// run without the worker intercepting requests.
if (!dev && "serviceWorker" in navigator) {
void navigator.serviceWorker.register("/service-worker.js");
void navigator.serviceWorker.register(withBase("/service-worker.js"), {
scope: withBase("/"),
});
}
return () => {
eventStream.stop();
@@ -75,7 +78,9 @@
streamSessionId = null;
}
const pathname = page.url.pathname;
// page.url.pathname includes the configured base path; strip it so
// the route comparisons below stay base-agnostic.
const pathname = page.url.pathname.slice(appBase.length);
// Debug-only routes under /__debug/* run their own bootstrap
// path against the storage primitives and must bypass the
// auth guard so Phase 6's Playwright spec can drive the
@@ -84,9 +89,9 @@
return;
}
if (session.status === "anonymous" && pathname !== "/login") {
void goto("/login", { replaceState: true });
void goto(withBase("/login"), { replaceState: true });
} else if (session.status === "authenticated" && pathname === "/login") {
void goto("/lobby", { replaceState: true });
void goto(withBase("/lobby"), { replaceState: true });
}
});
</script>
@@ -43,6 +43,7 @@ the next game's snapshot — and the next game's selection — start
fresh.
-->
<script lang="ts">
import { withBase } from "$lib/paths";
import { onDestroy, onMount, setContext, untrack } from "svelte";
import { goto } from "$app/navigation";
import { page } from "$app/state";
@@ -86,9 +87,9 @@ fresh.
import { session } from "$lib/session-store.svelte";
import { loadStore } from "../../../platform/store/index";
import { loadCore } from "../../../platform/core/index";
import { createEdgeGatewayClient } from "../../../api/connect";
import { createGatewayClient } from "../../../api/connect";
import { GalaxyClient } from "../../../api/galaxy-client";
import { GATEWAY_BASE_URL, GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env";
import { gatewayRpcBaseUrl, GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env";
import {
getSyntheticReport,
isSyntheticGameId,
@@ -373,7 +374,7 @@ fresh.
if (isSyntheticGameId(gameId)) {
const report = getSyntheticReport(gameId);
if (report === undefined) {
await goto("/lobby");
await goto(withBase("/lobby"));
return;
}
try {
@@ -420,7 +421,7 @@ fresh.
coreHolder.set(core);
const client = new GalaxyClient({
core,
edge: createEdgeGatewayClient(GATEWAY_BASE_URL),
edge: createGatewayClient(gatewayRpcBaseUrl()),
signer: (canonical) => keypair.sign(canonical),
sha256,
deviceSessionId,
@@ -472,7 +473,7 @@ fresh.
messageParams: { from: parsed.from },
actionLabelKey: "game.events.mail_new.action",
onAction: () => {
void goto(`/games/${gameId}/mail`);
void goto(withBase(`/games/${gameId}/mail`));
},
durationMs: 8000,
});
+7 -6
View File
@@ -1,8 +1,9 @@
<script lang="ts">
import { withBase } from "$lib/paths";
import { goto } from "$app/navigation";
import { onMount } from "svelte";
import { createEdgeGatewayClient } from "../../api/connect";
import { createGatewayClient } from "../../api/connect";
import { GalaxyClient } from "../../api/galaxy-client";
import {
LobbyError,
@@ -19,7 +20,7 @@
} from "../../api/lobby";
import { ByteBuffer } from "flatbuffers";
import { AccountResponse } from "../../proto/galaxy/fbs/user";
import { GATEWAY_BASE_URL, GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env";
import { gatewayRpcBaseUrl, GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env";
import {
SyntheticReportError,
loadSyntheticReportFromJSON,
@@ -184,11 +185,11 @@
}
function gotoCreate(): void {
goto("/lobby/create");
goto(withBase("/lobby/create"));
}
function gotoGame(gameId: string): void {
goto(`/games/${gameId}/map`);
goto(withBase(`/games/${gameId}/map`));
}
async function onSyntheticFileChange(
@@ -207,7 +208,7 @@
const text = await file.text();
const json: unknown = JSON.parse(text);
const { gameId } = loadSyntheticReportFromJSON(json);
await goto(`/games/${gameId}/map`);
await goto(withBase(`/games/${gameId}/map`));
} catch (err) {
if (err instanceof SyntheticReportError) {
syntheticError = err.message;
@@ -250,7 +251,7 @@
const core = await loadCore();
client = new GalaxyClient({
core,
edge: createEdgeGatewayClient(GATEWAY_BASE_URL),
edge: createGatewayClient(gatewayRpcBaseUrl()),
signer: (canonical) => keypair.sign(canonical),
sha256,
deviceSessionId: session.deviceSessionId,
@@ -1,11 +1,12 @@
<script lang="ts">
import { withBase } from "$lib/paths";
import { goto } from "$app/navigation";
import { onMount } from "svelte";
import { createEdgeGatewayClient } from "../../../api/connect";
import { createGatewayClient } from "../../../api/connect";
import { GalaxyClient } from "../../../api/galaxy-client";
import { LobbyError, createGame } from "../../../api/lobby";
import { GATEWAY_BASE_URL, GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env";
import { gatewayRpcBaseUrl, GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
import { loadCore } from "../../../platform/core/index";
import { session } from "$lib/session-store.svelte";
@@ -51,7 +52,7 @@
}
function cancel(): void {
goto("/lobby");
goto(withBase("/lobby"));
}
async function submit(): Promise<void> {
@@ -93,7 +94,7 @@
turnSchedule: trimmedSchedule,
targetEngineVersion: targetEngineVersion.trim() || DEFAULT_TARGET_ENGINE_VERSION,
});
goto("/lobby");
goto(withBase("/lobby"));
} catch (err) {
formError = describeLobbyError(err);
} finally {
@@ -116,7 +117,7 @@
const core = await loadCore();
client = new GalaxyClient({
core,
edge: createEdgeGatewayClient(GATEWAY_BASE_URL),
edge: createGatewayClient(gatewayRpcBaseUrl()),
signer: (canonical) => keypair.sign(canonical),
sha256,
deviceSessionId: session.deviceSessionId,
+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { withBase } from "$lib/paths";
import { goto } from "$app/navigation";
import {
AuthError,
@@ -88,7 +89,7 @@
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});
await session.signIn(result.deviceSessionId);
void goto("/lobby", { replaceState: true });
void goto(withBase("/lobby"), { replaceState: true });
} catch (err) {
if (err instanceof AuthError && err.code === "invalid_request") {
challengeId = null;
+7 -5
View File
@@ -9,14 +9,16 @@
//
// SvelteKit registers this worker automatically in the production build.
import { build, files, version } from "$service-worker";
import { base, build, files, version } from "$service-worker";
const sw = self as unknown as ServiceWorkerGlobalScope;
const CACHE = `galaxy-cache-${version}`;
// "/" is the SPA shell (adapter-static fallback); precaching it makes the
// start_url load offline.
const PRECACHE = ["/", ...build, ...files];
// `${base}/` is the SPA shell (adapter-static fallback); precaching it
// makes the start_url load offline. `base` is empty at the root and
// `/game` under the single-origin deployment, and `$service-worker`
// derives it from `location.pathname` so it stays correct in a subdir.
const PRECACHE = [`${base}/`, ...build, ...files];
sw.addEventListener("install", (event) => {
event.waitUntil(
@@ -65,7 +67,7 @@ sw.addEventListener("fetch", (event) => {
const cached = await cache.match(request);
if (cached) return cached;
if (request.mode === "navigate") {
const shell = await cache.match("/");
const shell = await cache.match(`${base}/`);
if (shell) return shell;
}
throw err;
+6 -6
View File
@@ -2,28 +2,28 @@
"name": "Galaxy",
"short_name": "Galaxy",
"description": "Galaxy — a turn-based space strategy game.",
"id": "/",
"start_url": "/",
"scope": "/",
"id": "./",
"start_url": "./",
"scope": "./",
"display": "standalone",
"orientation": "any",
"background_color": "#0a0e1a",
"theme_color": "#0a0e1a",
"icons": [
{
"src": "/icons/icon-192.png",
"src": "icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-512.png",
"src": "icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-maskable-512.png",
"src": "icons/icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
+7
View File
@@ -11,6 +11,13 @@ export default {
fallback: "index.html",
strict: true,
}),
paths: {
// Base path the app is served under. Empty by default so local
// dev, vitest, and Playwright run at the root unchanged; the
// deployed single-origin build sets BASE_PATH=/game and the
// edge Caddy serves the SPA under that prefix.
base: process.env.BASE_PATH ?? "",
},
serviceWorker: {
// Registered manually in the root layout for production only.
// SvelteKit's auto-registration also runs under `vite dev`, where
+4 -4
View File
@@ -8,13 +8,13 @@
// server picks up via `VITE_GATEWAY_RESPONSE_PUBLIC_KEY`.
//
// The Connect-Web request URL pattern is
// <baseUrl>/galaxy.gateway.v1.EdgeGateway/<MethodName>
// <baseUrl>/edge.v1.Gateway/<MethodName>
// so the route handlers below match against the trailing path
// suffix and ignore the host.
import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
import {
buildAccountResponsePayload,
@@ -54,7 +54,7 @@ async function mockGatewayHappyPath(
);
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -105,7 +105,7 @@ async function mockGatewayHappyPath(
);
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async (route) => {
// Hold the stream open until the test releases it via
// `pendingSubscribes`. Releasing fulfils with a Connect
+2 -2
View File
@@ -15,7 +15,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { ByteBuffer } from "flatbuffers";
import { expect, test, type Page } from "@playwright/test";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { GameBattleRequest } from "../../src/proto/galaxy/fbs/battle";
@@ -91,7 +91,7 @@ async function mockGatewayAndBattle(page: Page): Promise<void> {
};
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
+3 -3
View File
@@ -12,7 +12,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import {
CommandPlanetRouteRemove,
@@ -110,7 +110,7 @@ async function mockGateway(page: Page): Promise<MockHandle> {
let submitCount = 0;
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -267,7 +267,7 @@ async function mockGateway(page: Page): Promise<MockHandle> {
);
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async () => {
await new Promise<void>(() => {});
},
+1 -1
View File
@@ -18,7 +18,7 @@
import { create, toJsonString } from "@bufbuild/protobuf";
import { webcrypto } from "node:crypto";
import { GatewayEventSchema } from "../../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { GatewayEventSchema } from "../../../src/proto/edge/v1/edge_gateway_pb";
import { buildEventSigningInput } from "./canon";
import {
FIXTURE_PRIVATE_KEY_PKCS8_BASE64,
@@ -8,7 +8,7 @@
import { create, toJson, toJsonString } from "@bufbuild/protobuf";
import { webcrypto } from "node:crypto";
import { ExecuteCommandResponseSchema } from "../../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandResponseSchema } from "../../../src/proto/edge/v1/edge_gateway_pb";
import {
FIXTURE_PRIVATE_KEY_PKCS8_BASE64,
decodeBase64,
@@ -10,7 +10,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
@@ -46,7 +46,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<void> {
};
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -93,7 +93,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<void> {
);
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async () => {
await new Promise<void>(() => {});
},
+3 -3
View File
@@ -11,7 +11,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
@@ -53,7 +53,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockState> {
};
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -112,7 +112,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockState> {
// the watcher's catch path logs the abort and returns without a
// sign-out — same convention as `tests/e2e/lobby-flow.spec.ts`.
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async () => {
await new Promise<void>(() => {});
},
+3 -3
View File
@@ -30,7 +30,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
import {
@@ -70,7 +70,7 @@ async function mockGateway(page: Page): Promise<MockState> {
});
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -147,7 +147,7 @@ async function mockGateway(page: Page): Promise<MockState> {
);
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async () => {
await new Promise<void>(() => {});
},
+3 -3
View File
@@ -12,7 +12,7 @@
import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { GameCreateRequest } from "../../src/proto/galaxy/fbs/lobby";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
import {
@@ -74,7 +74,7 @@ async function mockGateway(page: Page, initial: Partial<LobbyState> = {}): Promi
});
});
await page.route("**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand", async (route) => {
await page.route("**/edge.v1.Gateway/ExecuteCommand", async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
await route.fulfill({ status: 400 });
@@ -208,7 +208,7 @@ async function mockGateway(page: Page, initial: Partial<LobbyState> = {}): Promi
});
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async (route) => {
const action = await new Promise<"endOfStream" | "abort">((resolve) => {
mocks.pendingSubscribes.push(() => resolve("endOfStream"));
+3 -3
View File
@@ -10,7 +10,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import { UserGamesOrderGet } from "../../src/proto/galaxy/fbs/order";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
@@ -44,7 +44,7 @@ async function mockGateway(page: Page): Promise<void> {
};
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -116,7 +116,7 @@ async function mockGateway(page: Page): Promise<void> {
);
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async () => {
await new Promise<void>(() => {});
},
+3 -3
View File
@@ -20,7 +20,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
@@ -57,7 +57,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<void> {
};
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -183,7 +183,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<void> {
// sign the session out mid-test (same convention as
// `game-shell-map.spec.ts`).
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async () => {
await new Promise<void>(() => {});
},
+3 -3
View File
@@ -9,7 +9,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import {
UserGamesOrder,
@@ -76,7 +76,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
let submitCalls = 0;
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -204,7 +204,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
let subscribeServed = false;
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async (route) => {
if (opts.subscribeFrame !== undefined && !subscribeServed) {
subscribeServed = true;
@@ -12,7 +12,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import {
CommandPlanetProduce,
@@ -74,7 +74,7 @@ async function mockGateway(page: Page): Promise<MockHandle> {
let submitCount = 0;
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -187,7 +187,7 @@ async function mockGateway(page: Page): Promise<MockHandle> {
);
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async () => {
await new Promise<void>(() => {});
},
+3 -3
View File
@@ -14,7 +14,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import {
CommandPayload,
@@ -65,7 +65,7 @@ async function mockGateway(page: Page): Promise<MockHandle> {
let lastVote: MockHandle["lastVote"] = null;
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -239,7 +239,7 @@ async function mockGateway(page: Page): Promise<MockHandle> {
);
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async () => {
await new Promise<void>(() => {});
},
+3 -3
View File
@@ -11,7 +11,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import {
UserGamesOrder,
@@ -65,7 +65,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
let lastReportName = "Earth";
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -181,7 +181,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
);
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async () => {
await new Promise<void>(() => {});
},
@@ -16,7 +16,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
@@ -92,7 +92,7 @@ async function mockGateway(page: Page): Promise<void> {
const storedOrder: CommandResultFixture[] = [];
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -195,7 +195,7 @@ async function mockGateway(page: Page): Promise<void> {
);
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async () => {
await new Promise<void>(() => {});
},
@@ -9,7 +9,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import {
CommandPayload,
@@ -51,7 +51,7 @@ async function mockGateway(page: Page): Promise<void> {
let storedOrder: CommandResultFixture[] = [];
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -155,7 +155,7 @@ async function mockGateway(page: Page): Promise<void> {
);
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async () => {
await new Promise<void>(() => {});
},
+3 -3
View File
@@ -22,7 +22,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import {
CommandPayload,
@@ -94,7 +94,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
const reportSciences: ScienceFixture[] = [...(opts.initialSciences ?? [])];
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -242,7 +242,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
);
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async () => {
await new Promise<void>(() => {});
},
+3 -3
View File
@@ -20,7 +20,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import {
CommandPayload,
@@ -88,7 +88,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
const reportClasses: ShipClassFixture[] = [...(opts.initialClasses ?? [])];
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -220,7 +220,7 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
);
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async () => {
await new Promise<void>(() => {});
},
+3 -3
View File
@@ -10,7 +10,7 @@ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { ExecuteCommandRequestSchema } from "../../src/proto/edge/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
@@ -52,7 +52,7 @@ async function mockGateway(page: Page): Promise<MockState> {
});
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
"**/edge.v1.Gateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
@@ -111,7 +111,7 @@ async function mockGateway(page: Page): Promise<MockState> {
// end-of-body) are held open indefinitely so the toast stays
// visible long enough for the test to interact with it.
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
"**/edge.v1.Gateway/SubscribeEvents",
async (route) => {
state.subscribeHits += 1;
if (state.subscribeHits === 1) {
+5 -5
View File
@@ -16,10 +16,10 @@ import {
createRouterTransport,
} from "@connectrpc/connect";
import {
EdgeGateway,
Gateway,
GatewayEventSchema,
type GatewayEvent,
} from "../src/proto/galaxy/gateway/v1/edge_gateway_pb";
} from "../src/proto/edge/v1/edge_gateway_pb";
let sessionStatus: "anonymous" | "authenticated" = "anonymous";
const signOutSpy = vi.fn();
@@ -91,9 +91,9 @@ function buildEvent(eventType: string, payload: Uint8Array): GatewayEvent {
function makeRouter(
streamFactory: () => AsyncIterable<GatewayEvent>,
): ReturnType<typeof createClient<typeof EdgeGateway>> {
): ReturnType<typeof createClient<typeof Gateway>> {
const transport = createRouterTransport(({ service }) => {
service(EdgeGateway, {
service(Gateway, {
executeCommand() {
throw new Error("not used in this test");
},
@@ -104,7 +104,7 @@ function makeRouter(
},
});
});
return createClient(EdgeGateway, transport);
return createClient(Gateway, transport);
}
describe("EventStream", () => {
+8 -8
View File
@@ -16,10 +16,10 @@ import { createClient, createRouterTransport } from "@connectrpc/connect";
import { describe, expect, test, vi } from "vitest";
import { GalaxyClient } from "../src/api/galaxy-client";
import {
EdgeGateway,
Gateway,
ExecuteCommandResponseSchema,
type ExecuteCommandRequest,
} from "../src/proto/galaxy/gateway/v1/edge_gateway_pb";
} from "../src/proto/edge/v1/edge_gateway_pb";
import type {
Core,
RequestSigningFields,
@@ -48,7 +48,7 @@ describe("GalaxyClient.executeCommand", () => {
const sha256 = vi.fn(async () => new Uint8Array(32).fill(0x33));
let captured: ExecuteCommandRequest | undefined;
const transport = createRouterTransport(({ service }) => {
service(EdgeGateway, {
service(Gateway, {
executeCommand(req) {
captured = req;
return create(ExecuteCommandResponseSchema, {
@@ -66,7 +66,7 @@ describe("GalaxyClient.executeCommand", () => {
},
});
});
const edge = createClient(EdgeGateway, transport);
const edge = createClient(Gateway, transport);
const client = new GalaxyClient({
core,
@@ -115,7 +115,7 @@ describe("GalaxyClient.executeCommand", () => {
verifyPayloadHashImpl: () => true,
});
const transport = createRouterTransport(({ service }) => {
service(EdgeGateway, {
service(Gateway, {
executeCommand: () =>
create(ExecuteCommandResponseSchema, {
protocolVersion: "v1",
@@ -133,7 +133,7 @@ describe("GalaxyClient.executeCommand", () => {
});
const client = new GalaxyClient({
core,
edge: createClient(EdgeGateway, transport),
edge: createClient(Gateway, transport),
signer: async () => new Uint8Array(64),
sha256: async () => new Uint8Array(32),
deviceSessionId: "device-session-1",
@@ -153,7 +153,7 @@ describe("GalaxyClient.executeCommand", () => {
verifyPayloadHashImpl: () => false,
});
const transport = createRouterTransport(({ service }) => {
service(EdgeGateway, {
service(Gateway, {
executeCommand: () =>
create(ExecuteCommandResponseSchema, {
protocolVersion: "v1",
@@ -171,7 +171,7 @@ describe("GalaxyClient.executeCommand", () => {
});
const client = new GalaxyClient({
core,
edge: createClient(EdgeGateway, transport),
edge: createClient(Gateway, transport),
signer: async () => new Uint8Array(64),
sha256: async () => new Uint8Array(32),
deviceSessionId: "device-session-1",
+2 -1
View File
@@ -39,11 +39,12 @@ vi.mock("../src/api/lobby", async () => {
vi.mock("../src/lib/env", () => ({
GATEWAY_BASE_URL: "http://gateway.test",
gatewayRpcBaseUrl: () => "http://gateway.test/rpc",
GATEWAY_RESPONSE_PUBLIC_KEY: new Uint8Array(32).fill(0x55),
}));
vi.mock("../src/api/connect", () => ({
createEdgeGatewayClient: vi.fn(() => ({})),
createGatewayClient: vi.fn(() => ({})),
}));
vi.mock("../src/api/galaxy-client", () => {
+2 -1
View File
@@ -55,11 +55,12 @@ vi.mock("../src/api/lobby", async () => {
vi.mock("../src/lib/env", () => ({
GATEWAY_BASE_URL: "http://gateway.test",
gatewayRpcBaseUrl: () => "http://gateway.test/rpc",
GATEWAY_RESPONSE_PUBLIC_KEY: new Uint8Array(32).fill(0x55),
}));
vi.mock("../src/api/connect", () => ({
createEdgeGatewayClient: vi.fn(() => ({})),
createGatewayClient: vi.fn(() => ({})),
}));
vi.mock("../src/api/galaxy-client", () => {
+2 -1
View File
@@ -78,9 +78,10 @@ export default defineConfig(({ mode }) => {
target: DEV_PROXY_TARGET,
changeOrigin: false,
},
"/galaxy.gateway.v1.EdgeGateway": {
"/rpc": {
target: DEV_GRPC_PROXY_TARGET,
changeOrigin: false,
rewrite: (path) => path.replace(/^\/rpc/, ""),
// Connect-Web server-streaming
// (`SubscribeEvents`) uses chunked HTTP
// responses; http-proxy passes them through