diff --git a/.changeset/curly-radios-visit.md b/.changeset/curly-radios-visit.md index b8ba395bb..f401b9bc9 100644 --- a/.changeset/curly-radios-visit.md +++ b/.changeset/curly-radios-visit.md @@ -9,5 +9,6 @@ Add a new `@trigger.dev/ai` package with: - rich default task payloads (`chatId`, trigger metadata, messages, request context) with optional payload mapping - reconnect-aware stream handling on top of Trigger.dev Realtime Streams v2 - strict `baseURL` normalization/validation (trimming, path-safe slash handling, absolute `http(s)` URLs only, no query/hash/credentials) +- rejection of internal whitespace characters in normalized `baseURL` values - deterministic baseURL validation error ordering for multi-issue inputs (protocol → query/hash → credentials) - explicit default `baseURL` behavior (`https://api.trigger.dev`) and case-insensitive `HTTP(S)` protocol acceptance diff --git a/docs/tasks/streams.mdx b/docs/tasks/streams.mdx index 421ba0f40..afa172077 100644 --- a/docs/tasks/streams.mdx +++ b/docs/tasks/streams.mdx @@ -671,11 +671,13 @@ Examples: - ❌ `https://user:pass@api.trigger.dev` - ❌ `ftp://api.trigger.dev` - ❌ `ws://api.trigger.dev` / `wss://api.trigger.dev` +- ❌ `https://api.trigger.dev/\ninternal` Validation errors use these exact messages: - `baseURL must not be empty` - `baseURL must be a valid absolute URL` +- `baseURL must not contain internal whitespace characters` - `baseURL must use http or https protocol` - `baseURL must not include query parameters or hash fragments` - `baseURL must not include username or password credentials` diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 190445329..201fdd03e 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -26,6 +26,7 @@ - Added explicit validation that `baseURL` uses `http` or `https`. - Added explicit validation that `baseURL` excludes query parameters and hash fragments. - Added explicit validation that `baseURL` excludes username/password credentials. +- Added explicit validation that `baseURL` excludes internal whitespace characters. - Documented that `HTTP://` and `HTTPS://` are accepted (case-insensitive protocol matching). - Added deterministic validation ordering for multi-issue baseURL values (protocol → query/hash → credentials). diff --git a/packages/ai/README.md b/packages/ai/README.md index bd75c9ccf..8d4d2fea2 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -178,11 +178,13 @@ Examples: - ❌ `https://user:pass@api.trigger.dev` (credentials) - ❌ `ftp://api.trigger.dev` (non-http protocol) - ❌ `ws://api.trigger.dev` / `wss://api.trigger.dev` (websocket protocols are rejected) +- ❌ `https://api.trigger.dev/\ninternal` (internal whitespace characters) Validation errors use these exact messages: - `baseURL must not be empty` - `baseURL must be a valid absolute URL` +- `baseURL must not contain internal whitespace characters` - `baseURL must use http or https protocol` - `baseURL must not include query parameters or hash fragments` - `baseURL must not include username or password credentials` diff --git a/packages/ai/src/chatTransport.test.ts b/packages/ai/src/chatTransport.test.ts index 0bc41f108..63039372f 100644 --- a/packages/ai/src/chatTransport.test.ts +++ b/packages/ai/src/chatTransport.test.ts @@ -720,6 +720,17 @@ describe("TriggerChatTransport", function () { }).toThrowError("baseURL must be a valid absolute URL"); }); + it("throws when baseURL contains internal whitespace characters", function () { + expect(function () { + new TriggerChatTransport({ + task: "chat-task", + accessToken: "pk_trigger", + baseURL: "https://api.trigger.dev/\ninternal", + stream: "chat-stream", + }); + }).toThrowError("baseURL must not contain internal whitespace characters"); + }); + it("throws when baseURL is a relative path", function () { expect(function () { new TriggerChatTransport({ @@ -3250,6 +3261,17 @@ describe("TriggerChatTransport", function () { }).toThrowError("baseURL must be a valid absolute URL"); }); + it("throws from factory when baseURL contains internal whitespace characters", function () { + expect(function () { + createTriggerChatTransport({ + task: "chat-task", + accessToken: "pk_trigger", + baseURL: "https://api.trigger.dev/\ninternal", + stream: "chat-stream", + }); + }).toThrowError("baseURL must not contain internal whitespace characters"); + }); + it("throws from factory when baseURL protocol is not http or https", function () { expect(function () { createTriggerChatTransport({ diff --git a/packages/ai/src/chatTransport.ts b/packages/ai/src/chatTransport.ts index c88a3ed1e..b05dfd17b 100644 --- a/packages/ai/src/chatTransport.ts +++ b/packages/ai/src/chatTransport.ts @@ -451,6 +451,7 @@ export function createTriggerChatTransport< const BASE_URL_VALIDATION_ERRORS = { empty: "baseURL must not be empty", invalidAbsoluteUrl: "baseURL must be a valid absolute URL", + containsWhitespace: "baseURL must not contain internal whitespace characters", invalidProtocol: "baseURL must use http or https protocol", queryOrHash: "baseURL must not include query parameters or hash fragments", credentials: "baseURL must not include username or password credentials", @@ -474,6 +475,8 @@ function normalizeBaseUrl(baseURL: string) { throw new Error(BASE_URL_VALIDATION_ERRORS.empty); } + assertBaseUrlHasNoInternalWhitespace(normalizedBaseUrl); + let parsedBaseUrl: URL; try { parsedBaseUrl = new URL(normalizedBaseUrl); @@ -488,6 +491,12 @@ function normalizeBaseUrl(baseURL: string) { return normalizedBaseUrl; } +function assertBaseUrlHasNoInternalWhitespace(baseUrl: string) { + if (/\s/.test(baseUrl)) { + throw new Error(BASE_URL_VALIDATION_ERRORS.containsWhitespace); + } +} + function assertValidBaseUrlProtocol(parsedBaseUrl: URL) { if ( parsedBaseUrl.protocol !== "http:" &&