Skip to content

Commit 17a0f07

Browse files
committed
fix(webapp): pin the AI-help redirect to the dashboard's own origin
new URL(environmentPath, origin) ignores origin when the path is absolute, and the result goes straight into redirect(). Today's only caller passes a builder-generated internal path, so this closes the gap rather than a hole.
1 parent f31e283 commit 17a0f07

2 files changed

Lines changed: 47 additions & 2 deletions

File tree

apps/webapp/app/components/dashboard-agent/ask-ai-channels.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,42 @@ describe("aiHelpRedirectUrl", () => {
7878
it("lands on the environment page", () => {
7979
expect(url.startsWith("https://cloud.trigger.dev/orgs/acme/projects/api/env/dev?")).toBe(true);
8080
});
81+
82+
/**
83+
* The only caller passes a path its own builder made, so none of these are reachable today.
84+
* The guard is here rather than at the `redirect()` because this helper is the one place both
85+
* that route and any future caller go through, and it is the only pure one of the two.
86+
*/
87+
it("stays on `origin` whatever shape the path arrives in", () => {
88+
const off = (environmentPath: string) =>
89+
new URL(
90+
aiHelpRedirectUrl({
91+
environmentPath,
92+
origin: "https://cloud.trigger.dev",
93+
query: "why",
94+
})
95+
);
96+
97+
expect(off("https://evil.example/steal").origin).toBe("https://cloud.trigger.dev");
98+
expect(off("//evil.example/steal").origin).toBe("https://cloud.trigger.dev");
99+
expect(off("https://evil.example//steal").origin).toBe("https://cloud.trigger.dev");
100+
expect(off("javascript:alert(1)").origin).toBe("https://cloud.trigger.dev");
101+
});
102+
103+
it("keeps the path, search and fragment of a normal internal path", () => {
104+
const parsed = new URL(
105+
aiHelpRedirectUrl({
106+
environmentPath: "/orgs/acme/projects/api/env/dev?tab=runs#top",
107+
origin: "https://cloud.trigger.dev",
108+
query: "why",
109+
})
110+
);
111+
112+
expect(parsed.pathname).toBe("/orgs/acme/projects/api/env/dev");
113+
expect(parsed.searchParams.get("tab")).toBe("runs");
114+
expect(parsed.searchParams.get("aiHelp")).toBe("why");
115+
expect(parsed.hash).toBe("#top");
116+
});
81117
});
82118

83119
/**

apps/webapp/app/components/dashboard-agent/ask-ai-channels.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ export function agentDeepLinkParams(availability: AskAiAvailability): readonly D
4747
return askAiChannelTarget(availability) === "ask-ai" ? NO_PARAMS : ASK_AI_PARAMS;
4848
}
4949

50-
/** Where `trigger dev`'s "Get a fix for this error using AI" link lands. */
50+
/**
51+
* Where `trigger dev`'s "Get a fix for this error using AI" link lands. Always on `origin`: an
52+
* absolute or protocol-relative `environmentPath` would otherwise decide the host itself, and
53+
* the caller feeds this straight to `redirect()`.
54+
*/
5155
export function aiHelpRedirectUrl({
5256
environmentPath,
5357
origin,
@@ -57,7 +61,12 @@ export function aiHelpRedirectUrl({
5761
origin: string;
5862
query: string;
5963
}): string {
60-
const url = new URL(environmentPath, origin);
64+
const base = new URL(origin);
65+
const requested = new URL(environmentPath, base);
66+
// Exactly one leading slash: a `javascript:` path has none and would run into the host, and
67+
// two would read as the start of another authority.
68+
const path = `/${requested.pathname.replace(/^\/+/, "")}`;
69+
const url = new URL(base.origin + path + requested.search + requested.hash);
6170
url.searchParams.set(ASK_AI_DEEP_LINK_PARAM, query);
6271
return url.toString();
6372
}

0 commit comments

Comments
 (0)