Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/message-transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
19 changes: 15 additions & 4 deletions src/message-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Console, "debug">;
}

export class PostMessageTransport implements Transport {
private messageListener: (
this: Window,
Expand All @@ -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"
Expand All @@ -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,
Expand Down Expand Up @@ -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, "*");
}
Expand Down