diff --git a/extensions/slack/src/monitor/relay-source.test.ts b/extensions/slack/src/monitor/relay-source.test.ts index 8b15c60a4572..503526282c56 100644 --- a/extensions/slack/src/monitor/relay-source.test.ts +++ b/extensions/slack/src/monitor/relay-source.test.ts @@ -6,6 +6,8 @@ import { buildRelayWebSocketOptions, buildRelayWebSocketUrl, monitorSlackRelaySource, + parseRelayFrame, + SlackRelayMalformedFrameError, SLACK_RELAY_MAX_PAYLOAD_BYTES, type SlackRelayIdentity, } from "./relay-source.js"; @@ -20,6 +22,10 @@ function deferred() { return { promise, reject, resolve }; } +function relayFrame(text: string): Buffer { + return Buffer.from(text, "utf8"); +} + describe("Slack relay source", () => { it("builds authenticated relay websocket URLs safely", () => { expect( @@ -198,4 +204,39 @@ describe("Slack relay source", () => { }); }); }); + + describe("parseRelayFrame", () => { + it("parses valid JSON frames", () => { + const frame = parseRelayFrame( + relayFrame(JSON.stringify({ type: "slack_event", data: { text: "hello" } })), + ); + expect(frame).toEqual({ type: "slack_event", data: { text: "hello" } }); + }); + + it("throws SlackRelayMalformedFrameError for malformed JSON", () => { + expect(() => parseRelayFrame(relayFrame("NOT JSON {{{"))).toThrow( + SlackRelayMalformedFrameError, + ); + }); + + it("wraps the original SyntaxError as the cause", () => { + let error: unknown; + try { + parseRelayFrame(relayFrame("NOT JSON {{{")); + } catch (err: unknown) { + error = err; + } + expect(error).toBeInstanceOf(SlackRelayMalformedFrameError); + expect((error as SlackRelayMalformedFrameError).message).toContain("malformed JSON frame"); + expect((error as SlackRelayMalformedFrameError).cause).toBeDefined(); + }); + + it("parses empty object frames", () => { + expect(parseRelayFrame(relayFrame("{}"))).toEqual({}); + }); + + it("parses array frames", () => { + expect(parseRelayFrame(relayFrame("[1, 2, 3]"))).toEqual([1, 2, 3]); + }); + }); }); diff --git a/extensions/slack/src/monitor/relay-source.ts b/extensions/slack/src/monitor/relay-source.ts index 7ef7ba75341c..6dd118c4aa21 100644 --- a/extensions/slack/src/monitor/relay-source.ts +++ b/extensions/slack/src/monitor/relay-source.ts @@ -293,9 +293,20 @@ function isLocalRelayHost(hostname: string): boolean { return isIP(host) === 4 && host.startsWith("127."); } -function parseRelayFrame(data: RawData): unknown { +export class SlackRelayMalformedFrameError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "SlackRelayMalformedFrameError"; + } +} + +export function parseRelayFrame(data: RawData): unknown { const text = rawDataToString(data); - return JSON.parse(text) as unknown; + try { + return JSON.parse(text) as unknown; + } catch (cause) { + throw new SlackRelayMalformedFrameError("Slack relay received malformed JSON frame", { cause }); + } } function rawDataToString(data: RawData): string {