Skip to content

Commit 9cba92b

Browse files
committed
feat(trigger-sdk): add streamBaseURL to TriggerChatTransport
Splits the SSE subscription URL from the append URL so the long-lived session.out stream can be routed through a custom proxy (e.g. a Cloudflare worker capturing JA4 fingerprints for bot detection) while keeping .in/append POSTs direct to baseURL. Falls back to baseURL when unset, so existing transports are unchanged.
1 parent a8280f1 commit 9cba92b

2 files changed

Lines changed: 46 additions & 1 deletion

File tree

packages/trigger-sdk/src/v3/chat.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,40 @@ describe("TriggerChatTransport", () => {
568568
expect(subscribe!).toContain("/realtime/v1/sessions/chat-by-chatid/out");
569569
});
570570

571+
it("routes .out SSE through streamBaseURL while appends stay on baseURL", async () => {
572+
const requests: string[] = [];
573+
global.fetch = vi.fn().mockImplementation(async (url: string | URL) => {
574+
const urlStr = typeof url === "string" ? url : url.toString();
575+
requests.push(urlStr);
576+
if (isSessionStreamAppendUrl(urlStr)) return defaultAppendResponse();
577+
if (isSessionOutSubscribeUrl(urlStr)) return defaultSseResponse();
578+
throw new Error(`Unexpected URL: ${urlStr}`);
579+
});
580+
581+
const transport = new TriggerChatTransport({
582+
task: "my-chat-task",
583+
accessToken: () => "pat",
584+
baseURL: "https://api.test.trigger.dev",
585+
streamBaseURL: "https://chat-proxy.example.com",
586+
sessions: { "chat-split": { publicAccessToken: "p" } },
587+
});
588+
589+
const stream = await transport.sendMessages({
590+
trigger: "submit-message",
591+
chatId: "chat-split",
592+
messageId: undefined,
593+
messages: [createUserMessage("Hi")],
594+
abortSignal: undefined,
595+
});
596+
await drainChunks(stream);
597+
598+
const append = requests.find(isSessionStreamAppendUrl);
599+
const subscribe = requests.find(isSessionOutSubscribeUrl);
600+
expect(append!.startsWith("https://api.test.trigger.dev/")).toBe(true);
601+
expect(subscribe!.startsWith("https://chat-proxy.example.com/")).toBe(true);
602+
expect(subscribe!).toContain("/realtime/v1/sessions/chat-split/out");
603+
});
604+
571605
it("for submit-message, only the latest message is delivered to .in", async () => {
572606
// Slim wire: each `.in/append` carries at most ONE new message in
573607
// `payload.message` (singular). Even if the caller hands sendMessages

packages/trigger-sdk/src/v3/chat.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,15 @@ export type TriggerChatTransportOptions<TClientData = unknown> = {
225225
/** Base URL for the Trigger.dev API. @default "https://api.trigger.dev" */
226226
baseURL?: string;
227227

228+
/**
229+
* Base URL for the SSE stream subscription only (`GET .../sessions/{chatId}/out`).
230+
* Falls back to `baseURL` when unset. Set this to route the long-lived
231+
* stream through a custom proxy (e.g. a Cloudflare worker capturing JA4
232+
* fingerprints for bot detection) while keeping append POSTs direct to
233+
* `baseURL` to avoid an extra hop on every user message.
234+
*/
235+
streamBaseURL?: string;
236+
228237
/** Additional headers included in every API request. */
229238
headers?: Record<string, string>;
230239

@@ -346,6 +355,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
346355
| ((params: StartSessionParams<Record<string, unknown>>) => Promise<StartSessionResult>)
347356
| undefined;
348357
private readonly baseURL: string;
358+
private readonly streamBaseURL: string;
349359
private readonly extraHeaders: Record<string, string>;
350360
private readonly streamTimeoutSeconds: number;
351361
private defaultMetadata: Record<string, unknown> | undefined;
@@ -367,6 +377,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
367377
| ((params: StartSessionParams<Record<string, unknown>>) => Promise<StartSessionResult>)
368378
| undefined;
369379
this.baseURL = options.baseURL ?? DEFAULT_BASE_URL;
380+
this.streamBaseURL = options.streamBaseURL ?? this.baseURL;
370381
this.extraHeaders = options.headers ?? {};
371382
this.streamTimeoutSeconds = options.streamTimeoutSeconds ?? DEFAULT_STREAM_TIMEOUT_SECONDS;
372383
this.defaultMetadata = options.clientData;
@@ -1021,7 +1032,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
10211032
);
10221033
}
10231034

1024-
const streamUrl = `${this.baseURL}/realtime/v1/sessions/${encodeURIComponent(chatId)}/out`;
1035+
const streamUrl = `${this.streamBaseURL}/realtime/v1/sessions/${encodeURIComponent(chatId)}/out`;
10251036

10261037
return new ReadableStream<UIMessageChunk>({
10271038
start: async (controller) => {

0 commit comments

Comments
 (0)