diff --git a/src/message-transport.test.ts b/src/message-transport.test.ts index e2518cce4..3b19c55cf 100644 --- a/src/message-transport.test.ts +++ b/src/message-transport.test.ts @@ -93,6 +93,60 @@ describe("PostMessageTransport", () => { return transport; } + describe("opt-in debug logging", () => { + it("debug logging is off by default", async () => { + const debug = mock(() => {}); + const originalDebug = console.debug; + console.debug = debug; + try { + const transport = await createStartedTransport(); + await transport.send(validRequest); + fakeWindow.dispatch("message", { + source: trustedSource, + data: validRequest, + }); + fakeWindow.dispatch("message", { + source: untrustedSource, + data: validRequest, + }); + fakeWindow.dispatch("message", { + source: trustedSource, + data: "not JSON-RPC", + }); + + expect(debug).not.toHaveBeenCalled(); + } finally { + console.debug = originalDebug; + } + }); + + it("logs only when an explicit logger is supplied", async () => { + const debug = mock((..._args: unknown[]) => {}); + const transport = new PostMessageTransport( + eventTarget as unknown as Window, + trustedSource as MessageEventSource, + { logger: { debug } }, + ); + await transport.start(); + await transport.send(validRequest); + fakeWindow.dispatch("message", { + source: trustedSource, + data: validRequest, + }); + fakeWindow.dispatch("message", { + source: untrustedSource, + data: validRequest, + }); + + expect(debug).toHaveBeenCalledTimes(3); + expect(debug.mock.calls.map((call) => call[0])).toEqual([ + "Sending message", + "Parsed message", + "Ignoring message from unknown source", + ]); + }); + }); + // ========================================================================== // Source validation — the security boundary at this layer. // The transport validates `event.source` (window identity), not `event.origin`. diff --git a/src/message-transport.ts b/src/message-transport.ts index afc85e935..0531d177e 100644 --- a/src/message-transport.ts +++ b/src/message-transport.ts @@ -41,6 +41,12 @@ import { TOOL_INPUT_PARTIAL_METHOD } from "./spec.types.js"; * @see {@link app!App.connect `App.connect`} for View usage * @see {@link app-bridge!AppBridge.connect `AppBridge.connect`} for Host usage */ +/** Optional, explicitly enabled diagnostics for postMessage transports. */ +export interface PostMessageTransportOptions { + /** Debug logging is disabled unless a logger is supplied. */ + logger?: Pick; +} + export class PostMessageTransport implements Transport { private messageListener: ( this: Window, @@ -53,6 +59,7 @@ export class PostMessageTransport implements Transport { * @param eventTarget - Target window to send messages to (default: `window.parent`) * @param eventSource - Source window for message validation. For views, pass * `window.parent`. For hosts, pass `iframe.contentWindow`. + * @param options - Optional diagnostics logger; no debug output by default. * * @example View connecting to parent * ```ts source="./message-transport.examples.ts#PostMessageTransport_constructor_view" @@ -71,20 +78,24 @@ export class PostMessageTransport implements Transport { constructor( private eventTarget: Window = window.parent, private eventSource: MessageEventSource, + private options: PostMessageTransportOptions = {}, ) { this.messageListener = (event) => { if (eventSource && event.source !== this.eventSource) { - console.debug("Ignoring message from unknown source", event); + this.options.logger?.debug( + "Ignoring message from unknown source", + event, + ); return; } const parsed = JSONRPCMessageSchema.safeParse(event.data); if (parsed.success) { - console.debug("Parsed message", parsed.data); + this.options.logger?.debug("Parsed message", parsed.data); this.onmessage?.(parsed.data); } else if (event.data?.jsonrpc !== "2.0") { // Not a JSON-RPC message at all (e.g. internal frames injected by // the host environment). Ignore silently so the transport stays alive. - console.debug( + this.options.logger?.debug( "Ignoring non-JSON-RPC message", parsed.error.message, event, @@ -125,7 +136,7 @@ export class PostMessageTransport implements Transport { // Skip debug log for high-frequency streaming notifications — these // can fire dozens of times per second and flood the console. if ((message as { method?: string }).method !== TOOL_INPUT_PARTIAL_METHOD) { - console.debug("Sending message", message); + this.options.logger?.debug("Sending message", message); } this.eventTarget.postMessage(message, "*"); }