fbc0260720
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>
95 lines
2.7 KiB
TypeScript
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());
|
|
}
|