feat(runner): WS protocol server with handshake
apps/runner scaffold (app-tier, walking-skeleton story 04): WS server speaking @repo/core-runner-protocol. Every inbound/outbound frame is envelope-wrapped and zod-parsed; hello/ready handshake gates on the workspace-scoped token (constant-time compare, redacted token on rejection replies); named error events for version/schema/auth rejections. Config via env only (token never argv); port announced on stdout for the story-06 provisioner. Runtime deps: ws (the standard Node WS server; ADR-022 traces do not apply to app-tier) and zod. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
98
apps/runner/src/server.test.ts
Normal file
98
apps/runner/src/server.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { PROTOCOL_VERSION } from "@repo/core-runner-protocol";
|
||||
import { gateInbound } from "@/server";
|
||||
|
||||
const TOKEN = "workspace-token";
|
||||
|
||||
function frame(overrides: Record<string, unknown> = {}): string {
|
||||
return JSON.stringify({
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
token: TOKEN,
|
||||
message: { type: "hello" },
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
describe("gateInbound", () => {
|
||||
it("accepts a conformant envelope and returns the inner message", () => {
|
||||
const result = gateInbound(frame(), TOKEN);
|
||||
expect(result).toEqual({ ok: true, message: { type: "hello" } });
|
||||
});
|
||||
|
||||
it("rejects a frame that is not JSON", () => {
|
||||
const result = gateInbound("{nope", TOKEN);
|
||||
expect(result).toMatchObject({ ok: false, cause: "invalid-message" });
|
||||
});
|
||||
|
||||
it("rejects JSON that is not an object", () => {
|
||||
expect(gateInbound('"hello"', TOKEN)).toMatchObject({
|
||||
ok: false,
|
||||
cause: "invalid-message",
|
||||
});
|
||||
expect(gateInbound("[1,2]", TOKEN)).toMatchObject({
|
||||
ok: false,
|
||||
cause: "invalid-message",
|
||||
});
|
||||
expect(gateInbound("null", TOKEN)).toMatchObject({
|
||||
ok: false,
|
||||
cause: "invalid-message",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects an unsupported protocol version with the named cause", () => {
|
||||
const result = gateInbound(frame({ protocolVersion: "999" }), TOKEN);
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
cause: "unsupported-protocol-version",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a missing protocol version as unsupported", () => {
|
||||
const raw = JSON.stringify({ token: TOKEN, message: { type: "hello" } });
|
||||
expect(gateInbound(raw, TOKEN)).toMatchObject({
|
||||
ok: false,
|
||||
cause: "unsupported-protocol-version",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a missing token as invalid-message (schema)", () => {
|
||||
const raw = JSON.stringify({
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
message: { type: "hello" },
|
||||
});
|
||||
expect(gateInbound(raw, TOKEN)).toMatchObject({
|
||||
ok: false,
|
||||
cause: "invalid-message",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects unknown envelope fields (strict schema)", () => {
|
||||
const result = gateInbound(frame({ extra: "field" }), TOKEN);
|
||||
expect(result).toMatchObject({ ok: false, cause: "invalid-message" });
|
||||
});
|
||||
|
||||
it("rejects an unknown message shape without echoing payload values", () => {
|
||||
const result = gateInbound(
|
||||
frame({
|
||||
message: { type: "clone", gitUrl: "", pat: "super-secret-pat" },
|
||||
}),
|
||||
TOKEN,
|
||||
);
|
||||
expect(result).toMatchObject({ ok: false, cause: "invalid-message" });
|
||||
expect(JSON.stringify(result)).not.toContain("super-secret-pat");
|
||||
});
|
||||
|
||||
it("rejects a wrong token as unauthorized", () => {
|
||||
expect(gateInbound(frame({ token: "wrong" }), TOKEN)).toMatchObject({
|
||||
ok: false,
|
||||
cause: "unauthorized",
|
||||
});
|
||||
// Same length, different bytes — exercises the constant-time compare.
|
||||
expect(
|
||||
gateInbound(frame({ token: "workspace-tokeX" }), TOKEN),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
cause: "unauthorized",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user