// `forgeGatewayEventFrame` produces one Connect HTTP/1.1 // server-streaming frame carrying a `GatewayEvent` signed with the // fixture private key. The Playwright `turn-ready.spec.ts` route // handler returns this body when the UI opens `SubscribeEvents` so // the production verification path (`core.verifyEvent`) accepts the // frame under the matching public key the dev server picks up via // `VITE_GATEWAY_RESPONSE_PUBLIC_KEY`. // // Connect HTTP/1.1 server-streaming framing per request: // 1 byte flag (0x00 = message) // 4 bytes length (big-endian, payload size) // N bytes payload (JSON-encoded GatewayEvent for the JSON codec) // // The route handler closes the response after one frame; the UI's // `events.svelte.ts` reconnect loop treats the abrupt end-of-body as // a transient error and backs off, which keeps the toast visible // long enough for the test to assert on it. import { create, toJsonString } from "@bufbuild/protobuf"; import { webcrypto } from "node:crypto"; import { GatewayEventSchema } from "../../../src/proto/galaxy/gateway/v1/edge_gateway_pb"; import { buildEventSigningInput } from "./canon"; import { FIXTURE_PRIVATE_KEY_PKCS8_BASE64, decodeBase64, } from "./gateway-key"; export interface ForgedEventInput { eventType: string; eventId: string; timestampMs: bigint; requestId: string; traceId: string; payloadBytes: Uint8Array; } let cachedPrivateKey: CryptoKey | null = null; async function privateKey(): Promise { if (cachedPrivateKey !== null) { return cachedPrivateKey; } const pkcs8 = decodeBase64(FIXTURE_PRIVATE_KEY_PKCS8_BASE64); cachedPrivateKey = await webcrypto.subtle.importKey( "pkcs8", pkcs8, { name: "Ed25519" }, false, ["sign"], ); return cachedPrivateKey; } async function sha256(payload: Uint8Array): Promise { const digest = await webcrypto.subtle.digest("SHA-256", payload); return new Uint8Array(digest); } export async function forgeGatewayEventFrame( input: ForgedEventInput, ): Promise { const payloadHash = await sha256(input.payloadBytes); const canonical = buildEventSigningInput({ eventType: input.eventType, eventId: input.eventId, timestampMs: input.timestampMs, requestId: input.requestId, traceId: input.traceId, payloadHash, }); const signatureBuf = await webcrypto.subtle.sign( { name: "Ed25519" }, await privateKey(), canonical, ); const event = create(GatewayEventSchema, { eventType: input.eventType, eventId: input.eventId, timestampMs: input.timestampMs, payloadBytes: input.payloadBytes, payloadHash, signature: new Uint8Array(signatureBuf), requestId: input.requestId, traceId: input.traceId, }); const body = new TextEncoder().encode( toJsonString(GatewayEventSchema, event), ); const frame = new Uint8Array(5 + body.length); frame[0] = 0x00; // message frame new DataView(frame.buffer).setUint32(1, body.length, false); frame.set(body, 5); return frame; }