From dec91eb6b7281efedae4ee37d6a88c2f8580dcb6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 15 Feb 2026 05:53:16 +0000 Subject: [PATCH] Cover BOM wrapper and internal BOM baseURL handling Co-authored-by: Eric Allam --- .changeset/curly-radios-visit.md | 2 +- docs/tasks/streams.mdx | 2 ++ packages/ai/CHANGELOG.md | 2 +- packages/ai/README.md | 2 ++ packages/ai/src/chatTransport.test.ts | 44 +++++++++++++++++++++++++++ 5 files changed, 50 insertions(+), 2 deletions(-) diff --git a/.changeset/curly-radios-visit.md b/.changeset/curly-radios-visit.md index b71656ebd..3b7471d1c 100644 --- a/.changeset/curly-radios-visit.md +++ b/.changeset/curly-radios-visit.md @@ -10,6 +10,6 @@ Add a new `@trigger.dev/ai` package with: - 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 -- rejection of internal invisible separator characters (e.g. zero-width spaces) in normalized `baseURL` values +- rejection of internal invisible separator characters (e.g. zero-width/BOM characters) in normalized `baseURL` values - deterministic baseURL validation error ordering for multi-issue inputs (internal whitespace → 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 ee5c3ecc9..282926ffb 100644 --- a/docs/tasks/streams.mdx +++ b/docs/tasks/streams.mdx @@ -668,6 +668,7 @@ Examples: - ✅ ` https://api.trigger.dev/custom-prefix/// ` (trimmed + normalized) - ✅ `https://api.trigger.dev/custom%20prefix` (percent-encoded whitespace) - ✅ `https://api.trigger.dev/custom%3Fprefix%23segment` (percent-encoded `?` / `#`) +- ✅ `\uFEFFhttps://api.trigger.dev/custom-prefix/\uFEFF` (BOM wrapper trimmed) - ❌ `https://api.trigger.dev?foo=bar` - ❌ `https://api.trigger.dev#fragment` - ❌ `https://user:pass@api.trigger.dev` @@ -677,6 +678,7 @@ Examples: - ❌ `https://api.trigger.dev/\tinternal` - ❌ `https://api.trigger.dev/\rinternal` - ❌ `https://api.trigger.dev/\u200Binternal` +- ❌ `https://api.trigger.dev/\uFEFFinternal` Validation errors use these exact messages: diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index e58889b73..1a9ccec21 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -26,7 +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/invisible separator characters. +- Added explicit validation that `baseURL` excludes internal whitespace/invisible separator characters (including zero-width/BOM characters). - Documented that `HTTP://` and `HTTPS://` are accepted (case-insensitive protocol matching). - Added deterministic validation ordering for multi-issue baseURL values (internal whitespace → protocol → query/hash → credentials). diff --git a/packages/ai/README.md b/packages/ai/README.md index 40ff531c8..38422d284 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -175,6 +175,7 @@ Examples: - ✅ ` https://api.trigger.dev/custom-prefix/// ` (trimmed + normalized) - ✅ `https://api.trigger.dev/custom%20prefix` (percent-encoded whitespace) - ✅ `https://api.trigger.dev/custom%3Fprefix%23segment` (percent-encoded `?` / `#`) +- ✅ `\uFEFFhttps://api.trigger.dev/custom-prefix/\uFEFF` (BOM wrapper trimmed) - ❌ `https://api.trigger.dev?foo=bar` (query string) - ❌ `https://api.trigger.dev#fragment` (hash fragment) - ❌ `https://user:pass@api.trigger.dev` (credentials) @@ -184,6 +185,7 @@ Examples: - ❌ `https://api.trigger.dev/\tinternal` (internal tab characters) - ❌ `https://api.trigger.dev/\rinternal` (internal carriage-return characters) - ❌ `https://api.trigger.dev/\u200Binternal` (internal zero-width-space characters) +- ❌ `https://api.trigger.dev/\uFEFFinternal` (internal BOM characters) Validation errors use these exact messages: diff --git a/packages/ai/src/chatTransport.test.ts b/packages/ai/src/chatTransport.test.ts index 04525700f..26963f197 100644 --- a/packages/ai/src/chatTransport.test.ts +++ b/packages/ai/src/chatTransport.test.ts @@ -764,6 +764,17 @@ describe("TriggerChatTransport", function () { }).toThrowError("baseURL must not contain internal whitespace characters"); }); + it("throws when baseURL contains internal BOM characters", function () { + expect(function () { + new TriggerChatTransport({ + task: "chat-task", + accessToken: "pk_trigger", + baseURL: "https://api.trigger.dev/\uFEFFinternal", + stream: "chat-stream", + }); + }).toThrowError("baseURL must not contain internal whitespace characters"); + }); + it("throws when baseURL is a relative path", function () { expect(function () { new TriggerChatTransport({ @@ -1006,6 +1017,17 @@ describe("TriggerChatTransport", function () { }).not.toThrow(); }); + it("accepts BOM-wrapped baseURL values", function () { + expect(function () { + new TriggerChatTransport({ + task: "chat-task", + accessToken: "pk_trigger", + baseURL: "\uFEFFhttps://api.trigger.dev/custom-prefix/\uFEFF", + stream: "chat-stream", + }); + }).not.toThrow(); + }); + it("accepts percent-encoded whitespace in baseURL paths", function () { expect(function () { new TriggerChatTransport({ @@ -3393,6 +3415,17 @@ describe("TriggerChatTransport", function () { }).toThrowError("baseURL must not contain internal whitespace characters"); }); + it("throws from factory when baseURL contains internal BOM characters", function () { + expect(function () { + createTriggerChatTransport({ + task: "chat-task", + accessToken: "pk_trigger", + baseURL: "https://api.trigger.dev/\uFEFFinternal", + 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({ @@ -3624,6 +3657,17 @@ describe("TriggerChatTransport", function () { }).not.toThrow(); }); + it("accepts BOM-wrapped baseURL values from factory", function () { + expect(function () { + createTriggerChatTransport({ + task: "chat-task", + accessToken: "pk_trigger", + baseURL: "\uFEFFhttps://api.trigger.dev/custom-prefix/\uFEFF", + stream: "chat-stream", + }); + }).not.toThrow(); + }); + it("accepts percent-encoded whitespace in baseURL paths from factory", function () { expect(function () { createTriggerChatTransport({