Files
galaxy-game/ui/frontend/tests/wasm-core.test.ts
Ilia Denisov fbc0260720 phase 5: wasm core, GalaxyClient skeleton, Connect-Web stubs
Compile `ui/core` to WebAssembly via TinyGo (903 KB) and expose four
canonical-bytes / signature-verification functions on
`globalThis.galaxyCore` from `ui/wasm/main.go`. The TypeScript-side
`Core` interface plus a `WasmCore` adapter (browser + JSDOM loader)
bridge those into a typed shape, and a `GalaxyClient` skeleton wires
`Core.signRequest` → injected `Signer` → typed Connect client →
`Core.verifyPayloadHash` / `verifyResponse`.

Wire `ui/buf.gen.yaml` against the local
`@bufbuild/protoc-gen-es` v2 binary (devDependency) so the codegen
step does not depend on the buf.build BSR. Vitest covers the bridge
end-to-end: per-method WasmCore tests under JSDOM, byte-for-byte
canon parity against the gateway fixtures committed in Phase 3, and
a `GalaxyClient` orchestration test using
`createRouterTransport`. The committed `core.wasm` snapshot tracks
TinyGo output so contributors run `make wasm` only when `ui/core/`
changes; CI consumes the snapshot directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 12:58:37 +02:00

95 lines
2.7 KiB
TypeScript

import { createHash } from "node:crypto";
import { beforeAll, describe, expect, test } from "vitest";
import type { Core } from "../src/platform/core/index";
import { loadWasmCoreForTest } from "./setup-wasm";
let core: Core;
beforeAll(async () => {
core = await loadWasmCoreForTest();
});
describe("WasmCore.signRequest", () => {
test("returns canonical bytes that lead with the v1 domain marker", () => {
const fields = {
protocolVersion: "v1",
deviceSessionId: "device-session-1",
messageType: "user.account.get",
timestampMs: 1700000000000n,
requestId: "req-1",
payloadHash: sha256("payload"),
};
const canonical = core.signRequest(fields);
const marker = "galaxy-request-v1";
expect(canonical.length).toBeGreaterThan(marker.length);
expect(new TextDecoder().decode(canonical.slice(1, 1 + marker.length))).toBe(
marker,
);
});
});
describe("WasmCore.verifyResponse", () => {
test("rejects a flipped-bit signature", () => {
const publicKey = new Uint8Array(32);
const signature = new Uint8Array(64);
const fields = {
protocolVersion: "v1",
requestId: "req-1",
timestampMs: 1700000000000n,
resultCode: "ok",
payloadHash: sha256("payload"),
};
expect(core.verifyResponse(publicKey, signature, fields)).toBe(false);
});
test("returns false for malformed key/signature lengths", () => {
const fields = {
protocolVersion: "v1",
requestId: "req-1",
timestampMs: 1700000000000n,
resultCode: "ok",
payloadHash: sha256("payload"),
};
expect(
core.verifyResponse(new Uint8Array(8), new Uint8Array(64), fields),
).toBe(false);
});
});
describe("WasmCore.verifyEvent", () => {
test("rejects an empty signature", () => {
const fields = {
eventType: "gateway.server_time",
eventId: "evt-1",
timestampMs: 1700000000000n,
requestId: "req-1",
traceId: "trace-1",
payloadHash: sha256("event"),
};
expect(
core.verifyEvent(new Uint8Array(32), new Uint8Array(0), fields),
).toBe(false);
});
});
describe("WasmCore.verifyPayloadHash", () => {
test("accepts the SHA-256 of the supplied payload", () => {
const payload = new TextEncoder().encode("hello");
expect(core.verifyPayloadHash(payload, sha256("hello"))).toBe(true);
});
test("rejects a digest of unrelated bytes", () => {
const payload = new TextEncoder().encode("hello");
expect(core.verifyPayloadHash(payload, sha256("world"))).toBe(false);
});
test("rejects a payload_hash whose length is not 32", () => {
const payload = new TextEncoder().encode("hello");
expect(core.verifyPayloadHash(payload, new Uint8Array(16))).toBe(false);
});
});
function sha256(value: string): Uint8Array {
return new Uint8Array(createHash("sha256").update(value).digest());
}