History

Add /connect/<slug> integration deep link

Resolves the slug against the tenant catalog and forwards into the integration detail page's existing add-connection flow. Unknown slugs get a not-found state. Makes the link survive sign-in: reserve connect as an org slug so cloud's callback stops reading it as an org selector, and return self-host's login to the requested path (its gate swaps the page in without ever writing a returnTo).

Rhys Sullivan committed 3 months ago

Commit 587b2cc1 parent 33f410b17 files · +659 −19Browse files

Showing the changes to apps (6 of 17 files). Show all

Unified · Split

6 files
  1. apps/cloud/src/routeTree.gen.ts +25 −0
  2. apps/host-cloudflare/web/routeTree.gen.ts +25 −0
  3. apps/host-selfhost/src/auth/return-to.test.ts +45 −1
  4. apps/host-selfhost/src/auth/return-to.ts +29 −0
  5. apps/host-selfhost/web/login.tsx +5 −10
  6. apps/host-selfhost/web/routeTree.gen.ts +25 −0
apps/cloud/src/routeTree.gen.ts+25−0
@@ -23,6 +23,7 @@ import { Route as ApiKeysRouteImport } from './routes/app/api-keys' · 22 unmodified lines
2323 import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRouteImport } from './../../../packages/react/src/routes/toolkits.$toolkitSlug'
2424 import { Route as ResumeDotexecutionIdRouteImport } from './routes/app/resume.$executionId'
2525 import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRouteImport } from './../../../packages/react/src/routes/integrations.$namespace'
26+import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRouteImport } from './../../../packages/react/src/routes/connect.$integrationSlug'
2627 import { Route as Billing_DotplansRouteImport } from './routes/app/billing_.plans'
2728 import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRouteImport } from './../../../packages/react/src/routes/integrations.add.$pluginKey'
2829
@@ -107,6 +108,14 @@ const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRout · 78 unmodified lines
107108 getParentRoute: () => rootRouteImport,
108109 } as any,
109110 )
111+const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute =
112+ DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRouteImport.update(
113+ {
114+ id: '/{-$orgSlug}/connect/$integrationSlug',
115+ path: '/{-$orgSlug}/connect/$integrationSlug',
116+ getParentRoute: () => rootRouteImport,
117+ } as any,
118+ )
110119 const Billing_DotplansRoute = Billing_DotplansRouteImport.update({
111120 id: '/{-$orgSlug}/billing_/plans',
112121 path: '/{-$orgSlug}/billing/plans',
@@ -134,6 +143,7 @@ export interface FileRoutesByFullPath { · 21 unmodified lines
134143 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
135144 '/{-$orgSlug}/': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
136145 '/{-$orgSlug}/billing/plans': typeof Billing_DotplansRoute
146+ '/{-$orgSlug}/connect/$integrationSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute
137147 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
138148 '/{-$orgSlug}/resume/$executionId': typeof ResumeDotexecutionIdRoute
139149 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
@@ -152,6 +162,7 @@ export interface FileRoutesByTo { · 12 unmodified lines
152162 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
153163 '/{-$orgSlug}': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
154164 '/{-$orgSlug}/billing/plans': typeof Billing_DotplansRoute
165+ '/{-$orgSlug}/connect/$integrationSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute
155166 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
156167 '/{-$orgSlug}/resume/$executionId': typeof ResumeDotexecutionIdRoute
157168 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
@@ -171,6 +182,7 @@ export interface FileRoutesById { · 13 unmodified lines
171182 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
172183 '/{-$orgSlug}/': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
173184 '/{-$orgSlug}/billing_/plans': typeof Billing_DotplansRoute
185+ '/{-$orgSlug}/connect/$integrationSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute
174186 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
175187 '/{-$orgSlug}/resume/$executionId': typeof ResumeDotexecutionIdRoute
176188 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
@@ -191,6 +203,7 @@ export interface FileRouteTypes { · 14 unmodified lines
191203 | '/{-$orgSlug}/tools'
192204 | '/{-$orgSlug}/'
193205 | '/{-$orgSlug}/billing/plans'
206+ | '/{-$orgSlug}/connect/$integrationSlug'
194207 | '/{-$orgSlug}/integrations/$namespace'
195208 | '/{-$orgSlug}/resume/$executionId'
196209 | '/{-$orgSlug}/toolkits/$toolkitSlug'
@@ -209,6 +222,7 @@ export interface FileRouteTypes { · 12 unmodified lines
209222 | '/{-$orgSlug}/tools'
210223 | '/{-$orgSlug}'
211224 | '/{-$orgSlug}/billing/plans'
225+ | '/{-$orgSlug}/connect/$integrationSlug'
212226 | '/{-$orgSlug}/integrations/$namespace'
213227 | '/{-$orgSlug}/resume/$executionId'
214228 | '/{-$orgSlug}/toolkits/$toolkitSlug'
@@ -227,6 +241,7 @@ export interface FileRouteTypes { · 12 unmodified lines
227241 | '/{-$orgSlug}/tools'
228242 | '/{-$orgSlug}/'
229243 | '/{-$orgSlug}/billing_/plans'
244+ | '/{-$orgSlug}/connect/$integrationSlug'
230245 | '/{-$orgSlug}/integrations/$namespace'
231246 | '/{-$orgSlug}/resume/$executionId'
232247 | '/{-$orgSlug}/toolkits/$toolkitSlug'
@@ -246,6 +261,7 @@ export interface RootRouteChildren { · 13 unmodified lines
246261 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
247262 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
248263 Billing_DotplansRoute: typeof Billing_DotplansRoute
264+ DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute
249265 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
250266 ResumeDotexecutionIdRoute: typeof ResumeDotexecutionIdRoute
251267 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute
@@ -351,6 +367,13 @@ declare module '@tanstack/react-router' { · 99 unmodified lines
351367 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRouteImport
352368 parentRoute: typeof rootRouteImport
353369 }
370+ '/{-$orgSlug}/connect/$integrationSlug': {
371+ id: '/{-$orgSlug}/connect/$integrationSlug'
372+ path: '/{-$orgSlug}/connect/$integrationSlug'
373+ fullPath: '/{-$orgSlug}/connect/$integrationSlug'
374+ preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRouteImport
375+ parentRoute: typeof rootRouteImport
376+ }
354377 '/{-$orgSlug}/billing_/plans': {
355378 id: '/{-$orgSlug}/billing_/plans'
356379 path: '/{-$orgSlug}/billing/plans'
@@ -400,6 +423,8 @@ const rootRouteChildren: RootRouteChildren = { · 43 unmodified lines
400423 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute:
401424 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute,
402425 Billing_DotplansRoute: Billing_DotplansRoute,
426+ DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute:
427+ DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute,
403428 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute:
404429 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute,
405430 ResumeDotexecutionIdRoute: ResumeDotexecutionIdRoute,
apps/host-cloudflare/web/routeTree.gen.ts+25−0
@@ -17,6 +17,7 @@ import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPoliciesRouteImp · 16 unmodified lines
1717 import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRouteImport } from './../../../packages/react/src/routes/toolkits.$toolkitSlug'
1818 import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRouteImport } from './../../../packages/react/src/routes/resume.$executionId'
1919 import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRouteImport } from './../../../packages/react/src/routes/integrations.$namespace'
20+import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRouteImport } from './../../../packages/react/src/routes/connect.$integrationSlug'
2021 import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport } from './../../../packages/react/src/routes/plugins.$pluginId.$'
2122 import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRouteImport } from './../../../packages/react/src/routes/integrations.add.$pluginKey'
2223
@@ -75,6 +76,14 @@ const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRout · 52 unmodified lines
7576 getParentRoute: () => rootRouteImport,
7677 } as any,
7778 )
79+const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute =
80+ DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRouteImport.update(
81+ {
82+ id: '/{-$orgSlug}/connect/$integrationSlug',
83+ path: '/{-$orgSlug}/connect/$integrationSlug',
84+ getParentRoute: () => rootRouteImport,
85+ } as any,
86+ )
7887 const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRoute =
7988 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport.update(
8089 {
@@ -98,6 +107,7 @@ export interface FileRoutesByFullPath { · 17 unmodified lines
98107 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
99108 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
100109 '/{-$orgSlug}/': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
110+ '/{-$orgSlug}/connect/$integrationSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute
101111 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
102112 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute
103113 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
@@ -110,6 +120,7 @@ export interface FileRoutesByTo { · 6 unmodified lines
110120 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
111121 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
112122 '/{-$orgSlug}': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
123+ '/{-$orgSlug}/connect/$integrationSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute
113124 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
114125 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute
115126 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
@@ -123,6 +134,7 @@ export interface FileRoutesById { · 7 unmodified lines
123134 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
124135 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
125136 '/{-$orgSlug}/': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
137+ '/{-$orgSlug}/connect/$integrationSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute
126138 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
127139 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute
128140 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
@@ -137,6 +149,7 @@ export interface FileRouteTypes { · 8 unmodified lines
137149 | '/{-$orgSlug}/toolkits'
138150 | '/{-$orgSlug}/tools'
139151 | '/{-$orgSlug}/'
152+ | '/{-$orgSlug}/connect/$integrationSlug'
140153 | '/{-$orgSlug}/integrations/$namespace'
141154 | '/{-$orgSlug}/resume/$executionId'
142155 | '/{-$orgSlug}/toolkits/$toolkitSlug'
@@ -149,6 +162,7 @@ export interface FileRouteTypes { · 6 unmodified lines
149162 | '/{-$orgSlug}/toolkits'
150163 | '/{-$orgSlug}/tools'
151164 | '/{-$orgSlug}'
165+ | '/{-$orgSlug}/connect/$integrationSlug'
152166 | '/{-$orgSlug}/integrations/$namespace'
153167 | '/{-$orgSlug}/resume/$executionId'
154168 | '/{-$orgSlug}/toolkits/$toolkitSlug'
@@ -161,6 +175,7 @@ export interface FileRouteTypes { · 6 unmodified lines
161175 | '/{-$orgSlug}/toolkits'
162176 | '/{-$orgSlug}/tools'
163177 | '/{-$orgSlug}/'
178+ | '/{-$orgSlug}/connect/$integrationSlug'
164179 | '/{-$orgSlug}/integrations/$namespace'
165180 | '/{-$orgSlug}/resume/$executionId'
166181 | '/{-$orgSlug}/toolkits/$toolkitSlug'
@@ -174,6 +189,7 @@ export interface RootRouteChildren { · 7 unmodified lines
174189 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
175190 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
176191 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
192+ DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute
177193 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
178194 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute
179195 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute
@@ -238,6 +254,13 @@ declare module '@tanstack/react-router' { · 58 unmodified lines
238254 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRouteImport
239255 parentRoute: typeof rootRouteImport
240256 }
257+ '/{-$orgSlug}/connect/$integrationSlug': {
258+ id: '/{-$orgSlug}/connect/$integrationSlug'
259+ path: '/{-$orgSlug}/connect/$integrationSlug'
260+ fullPath: '/{-$orgSlug}/connect/$integrationSlug'
261+ preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRouteImport
262+ parentRoute: typeof rootRouteImport
263+ }
241264 '/{-$orgSlug}/plugins/$pluginId/$': {
242265 id: '/{-$orgSlug}/plugins/$pluginId/$'
243266 path: '/{-$orgSlug}/plugins/$pluginId/$'
@@ -281,6 +304,8 @@ const rootRouteChildren: RootRouteChildren = { · 37 unmodified lines
281304 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute,
282305 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute:
283306 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute,
307+ DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute:
308+ DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute,
284309 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute:
285310 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute,
286311 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute:
apps/host-selfhost/src/auth/return-to.test.ts+45−1
@@ -1,6 +1,12 @@
11 import { describe, expect, it } from "@effect/vitest";
22
3−import { isSafeReturnTo, loginPath, mcpAuthorizeResumeTarget, safeReturnTo } from "./return-to";
3+import {
4+ isSafeReturnTo,
5+ loginPath,
6+ mcpAuthorizeResumeTarget,
7+ postLoginTarget,
8+ safeReturnTo,
9+} from "./return-to";
410
511 describe("isSafeReturnTo", () => {
612 const safe = [
@@ -73,6 +79,44 @@ describe("mcpAuthorizeResumeTarget", () => { · 66 unmodified lines
7379 });
7480 });
7581
82+describe("postLoginTarget", () => {
83+ // Self-host renders the login page IN PLACE of the requested route without
84+ // navigating, so the live location is the only record of where the person was
85+ // headed. A connect deep link must survive sign-in on that basis alone.
86+ it("returns to the deep link the person actually opened", () => {
87+ expect(postLoginTarget({ pathname: "/connect/linear", search: "" })).toBe("/connect/linear");
88+ expect(postLoginTarget({ pathname: "/integrations/sentry", search: "?addAccount=1" })).toBe(
89+ "/integrations/sentry?addAccount=1",
90+ );
91+ });
92+
93+ it("lands on the dashboard when signing in from the bare login page", () => {
94+ // No deep link to resume, and echoing /login back would re-render the form.
95+ expect(postLoginTarget({ pathname: "/login", search: "" })).toBe("/");
96+ });
97+
98+ it("prefers an explicit returnTo over the current location", () => {
99+ expect(postLoginTarget({ pathname: "/login", search: "?returnTo=%2Fconnect%2Flinear" })).toBe(
100+ "/connect/linear",
101+ );
102+ });
103+
104+ it("prefers an MCP authorize resume over everything else", () => {
105+ const target = postLoginTarget({
106+ pathname: "/login",
107+ search:
108+ "?response_type=code&client_id=abc&redirect_uri=http%3A%2F%2Flocalhost%3A3118%2Fcallback",
109+ });
110+ expect(target.startsWith("/api/auth/mcp/authorize?")).toBe(true);
111+ });
112+
113+ it("never honors an unsafe location", () => {
114+ // `safeReturnTo` vets the echoed path too — an app-plane path is not a
115+ // place to land a freshly signed-in browser.
116+ expect(postLoginTarget({ pathname: "/api/auth/logout", search: "" })).toBe("/");
117+ });
118+});
119+
76120 describe("loginPath", () => {
77121 it("omits returnTo for the root", () => {
78122 expect(loginPath("/")).toBe("/login");
apps/host-selfhost/src/auth/return-to.ts+29−0
@@ -32,3 +32,32 @@ export const mcpAuthorizeResumeTarget = (search: string): string | null => { · 31 unmodified lines
3232 if (!params.get("client_id") || !params.get("redirect_uri")) return null;
3333 return `${MCP_AUTHORIZE_PATH}?${params.toString()}`;
3434 };
35+
36+const LOGIN_PATH = "/login";
37+
38+/**
39+ * Where the self-host login page sends someone after a successful sign-in, in
40+ * priority order:
41+ *
42+ * 1. an interrupted MCP OAuth authorize (the params ARE the request),
43+ * 2. an explicit safe `returnTo` (e.g. the integration OAuth callback),
44+ * 3. the URL they actually opened, and
45+ * 4. the dashboard.
46+ *
47+ * Step 3 is what makes a deep link like `/connect/linear` survive sign-in here.
48+ * Unlike cloud — which redirects to `/login?returnTo=…` — self-host's gate
49+ * swaps the login page in WITHOUT navigating, so the address bar still holds
50+ * the requested URL and no `returnTo` was ever written. `/login` itself is
51+ * excluded so signing in from the bare login page lands on the dashboard rather
52+ * than looping back to the form.
53+ */
54+export const postLoginTarget = (location: {
55+ readonly pathname: string;
56+ readonly search: string;
57+}): string =>
58+ mcpAuthorizeResumeTarget(location.search) ??
59+ safeReturnTo(new URLSearchParams(location.search).get("returnTo")) ??
60+ (location.pathname === LOGIN_PATH
61+ ? null
62+ : safeReturnTo(`${location.pathname}${location.search}`)) ??
63+ "/";
apps/host-selfhost/web/login.tsx+5−10
@@ -6,7 +6,7 @@ import { Label } from "@executor-js/react/components/label"; · 5 unmodified lines
66
77 import { authClient } from "./auth-client";
88 import { AuthLayout } from "./auth-layout";
9−import { mcpAuthorizeResumeTarget, safeReturnTo } from "../src/auth/return-to";
9+import { postLoginTarget } from "../src/auth/return-to";
1010
1111 // Self-host login: email + password sign-in via Better Auth. On success we
1212 // reload so the shared AuthProvider re-reads /account/me and the AuthGate swaps
@@ -17,15 +17,10 @@ import { mcpAuthorizeResumeTarget, safeReturnTo } from "../src/auth/return-to"; · 4 unmodified lines
1717 // redeeming an invite — either the full /join/<code> link, or by entering the
1818 // code here ("Have an invite code?"), which forwards to the same join page.
1919 export const LoginPage = () => {
20− const search = window.location.search;
21− // Where to go after sign-in: resume an interrupted MCP OAuth authorize if we
22− // arrived from one (Better Auth redirects it here with the OAuth params),
23− // otherwise honor a safe returnTo (e.g. an integration OAuth callback), else
24− // land on the dashboard.
25− const postLogin =
26− mcpAuthorizeResumeTarget(search) ??
27− safeReturnTo(new URLSearchParams(search).get("returnTo")) ??
28− "/";
20+ // Where to go after sign-in. The gate renders this page IN PLACE of the
21+ // requested route without navigating, so the live location is what carries a
22+ // deep link (`/connect/linear`) across sign-in — see `postLoginTarget`.
23+ const postLogin = postLoginTarget(window.location);
2924 const [mode, setMode] = useState<"signin" | "code">("signin");
3025 const [email, setEmail] = useState("");
3126 const [password, setPassword] = useState("");
apps/host-selfhost/web/routeTree.gen.ts+25−0
@@ -20,6 +20,7 @@ import { Route as JoinDotcodeRouteImport } from './routes/public/join.$code' · 19 unmodified lines
2020 import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRouteImport } from './../../../packages/react/src/routes/toolkits.$toolkitSlug'
2121 import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRouteImport } from './../../../packages/react/src/routes/resume.$executionId'
2222 import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRouteImport } from './../../../packages/react/src/routes/integrations.$namespace'
23+import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRouteImport } from './../../../packages/react/src/routes/connect.$integrationSlug'
2324 import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport } from './../../../packages/react/src/routes/plugins.$pluginId.$'
2425 import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRouteImport } from './../../../packages/react/src/routes/integrations.add.$pluginKey'
2526
@@ -93,6 +94,14 @@ const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRout · 67 unmodified lines
9394 getParentRoute: () => rootRouteImport,
9495 } as any,
9596 )
97+const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute =
98+ DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRouteImport.update(
99+ {
100+ id: '/{-$orgSlug}/connect/$integrationSlug',
101+ path: '/{-$orgSlug}/connect/$integrationSlug',
102+ getParentRoute: () => rootRouteImport,
103+ } as any,
104+ )
96105 const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRoute =
97106 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport.update(
98107 {
@@ -119,6 +128,7 @@ export interface FileRoutesByFullPath { · 20 unmodified lines
119128 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
120129 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
121130 '/{-$orgSlug}/': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
131+ '/{-$orgSlug}/connect/$integrationSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute
122132 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
123133 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute
124134 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
@@ -134,6 +144,7 @@ export interface FileRoutesByTo { · 9 unmodified lines
134144 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
135145 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
136146 '/{-$orgSlug}': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
147+ '/{-$orgSlug}/connect/$integrationSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute
137148 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
138149 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute
139150 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
@@ -150,6 +161,7 @@ export interface FileRoutesById { · 10 unmodified lines
150161 '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
151162 '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
152163 '/{-$orgSlug}/': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
164+ '/{-$orgSlug}/connect/$integrationSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute
153165 '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
154166 '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute
155167 '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsDottoolkitSlugRoute
@@ -167,6 +179,7 @@ export interface FileRouteTypes { · 11 unmodified lines
167179 | '/{-$orgSlug}/toolkits'
168180 | '/{-$orgSlug}/tools'
169181 | '/{-$orgSlug}/'
182+ | '/{-$orgSlug}/connect/$integrationSlug'
170183 | '/{-$orgSlug}/integrations/$namespace'
171184 | '/{-$orgSlug}/resume/$executionId'
172185 | '/{-$orgSlug}/toolkits/$toolkitSlug'
@@ -182,6 +195,7 @@ export interface FileRouteTypes { · 9 unmodified lines
182195 | '/{-$orgSlug}/toolkits'
183196 | '/{-$orgSlug}/tools'
184197 | '/{-$orgSlug}'
198+ | '/{-$orgSlug}/connect/$integrationSlug'
185199 | '/{-$orgSlug}/integrations/$namespace'
186200 | '/{-$orgSlug}/resume/$executionId'
187201 | '/{-$orgSlug}/toolkits/$toolkitSlug'
@@ -197,6 +211,7 @@ export interface FileRouteTypes { · 9 unmodified lines
197211 | '/{-$orgSlug}/toolkits'
198212 | '/{-$orgSlug}/tools'
199213 | '/{-$orgSlug}/'
214+ | '/{-$orgSlug}/connect/$integrationSlug'
200215 | '/{-$orgSlug}/integrations/$namespace'
201216 | '/{-$orgSlug}/resume/$executionId'
202217 | '/{-$orgSlug}/toolkits/$toolkitSlug'
@@ -213,6 +228,7 @@ export interface RootRouteChildren { · 10 unmodified lines
213228 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren
214229 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute
215230 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute
231+ DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute
216232 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute
217233 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute
218234 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute
@@ -298,6 +314,13 @@ declare module '@tanstack/react-router' { · 79 unmodified lines
298314 preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRouteImport
299315 parentRoute: typeof rootRouteImport
300316 }
317+ '/{-$orgSlug}/connect/$integrationSlug': {
318+ id: '/{-$orgSlug}/connect/$integrationSlug'
319+ path: '/{-$orgSlug}/connect/$integrationSlug'
320+ fullPath: '/{-$orgSlug}/connect/$integrationSlug'
321+ preLoaderRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRouteImport
322+ parentRoute: typeof rootRouteImport
323+ }
301324 '/{-$orgSlug}/plugins/$pluginId/$': {
302325 id: '/{-$orgSlug}/plugins/$pluginId/$'
303326 path: '/{-$orgSlug}/plugins/$pluginId/$'
@@ -344,6 +367,8 @@ const rootRouteChildren: RootRouteChildren = { · 40 unmodified lines
344367 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolsRoute,
345368 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute:
346369 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRoute,
370+ DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute:
371+ DotDotDotDotDotDotDotDotPackagesReactSrcRoutesConnectDotintegrationSlugRoute,
347372 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute:
348373 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotnamespaceRoute,
349374 DotDotDotDotDotDotDotDotPackagesReactSrcRoutesResumeDotexecutionIdRoute: