Fix connector Connect: use the link flow and surface failures
Root cause found by probing the live API: Composio v3 rejects POST /connected_accounts for Composio-managed OAuth auth configs with a 400 pointing at POST /connected_accounts/link, and the renderer threw the failure away inside a .then, so clicking Connect did nothing and no browser opened. - createConnectedAccount now calls POST /connected_accounts/link with auth_config_id and user_id and reads connected_account_id and redirect_url; the local mirror records INITIALIZING until the poll adopts Composio's real status. - Connect failures (and a blocked external-URL open) now show as a single line on the page instead of vanishing.
This commit is contained in:
1 parent
a2de132083
commit
d19573187c
4 files changed
+40
-16
No files matched your search
@@ -94,19 +94,21 @@ export class ComposioClient {
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Starts a connection for a user; the redirect URL completes it in the browser. */
|
||||
/**
|
||||
* Starts a connection for a user; the redirect URL completes it in the
|
||||
* browser. Composio-managed auth configs only accept the link flow —
|
||||
* POST /connected_accounts is rejected for them with a 400.
|
||||
*/
|
||||
async createConnectedAccount(input: { authConfigId: string; userId: string }): Promise<{
|
||||
id: string;
|
||||
status: string;
|
||||
redirectUrl: string | null;
|
||||
}> {
|
||||
const response = await this.request("POST", `${BASE_URL}/connected_accounts`, {
|
||||
auth_config: { id: input.authConfigId },
|
||||
connection: { user_id: input.userId },
|
||||
const response = await this.request("POST", `${BASE_URL}/connected_accounts/link`, {
|
||||
auth_config_id: input.authConfigId,
|
||||
user_id: input.userId,
|
||||
});
|
||||
return {
|
||||
id: String(response.id ?? ""),
|
||||
status: String(response.status ?? ""),
|
||||
id: String(response.connected_account_id ?? ""),
|
||||
redirectUrl: typeof response.redirect_url === "string" ? response.redirect_url : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,8 +26,10 @@ export class ComposioConnectionService {
|
||||
/**
|
||||
* Starts (or restarts) the company's connection for a toolkit: creates a
|
||||
* Composio-managed auth config once per toolkit, initiates the connected
|
||||
* account for the company user, and records the pending account locally.
|
||||
* The returned redirect URL completes authorization in the browser.
|
||||
* account for the company user via the link flow, and records the pending
|
||||
* account locally as INITIALIZING — the poll that follows adopts Composio's
|
||||
* real status. The returned redirect URL completes authorization in the
|
||||
* browser.
|
||||
*/
|
||||
async connectToolkit(orgId: string, toolkitSlug: string, toolkitName: string): Promise<ConnectComposioToolkitResult> {
|
||||
const slug = normalizeSlug(toolkitSlug);
|
||||
@@ -48,8 +50,8 @@ export class ComposioConnectionService {
|
||||
auth_config_id = excluded.auth_config_id,
|
||||
status = excluded.status,
|
||||
updated_at = excluded.updated_at
|
||||
`).run(account.id, orgId, slug, toolkitName.trim() || slug, authConfigId, account.status || "INITIATED", now, now);
|
||||
return { accountId: account.id, status: account.status, redirectUrl: account.redirectUrl };
|
||||
`).run(account.id, orgId, slug, toolkitName.trim() || slug, authConfigId, "INITIALIZING", now, now);
|
||||
return { accountId: account.id, redirectUrl: account.redirectUrl };
|
||||
}
|
||||
|
||||
/** Pulls account statuses from Composio and updates the local mirror. */
|
||||
|
||||
@@ -26,6 +26,7 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
|
||||
const employee = requireActiveEmployee(snapshot);
|
||||
const [catalogSlot, setCatalogSlot] = useState<CatalogSlot>(null);
|
||||
const [busySlug, setBusySlug] = useState<string | null>(null);
|
||||
const [connectError, setConnectError] = useState<string | null>(null);
|
||||
const pollTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const catalog: CatalogState = catalogSlot && catalogSlot.orgId === orgId ? catalogSlot.state : { phase: "loading" };
|
||||
|
||||
@@ -90,14 +91,19 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
|
||||
|
||||
const connect = (toolkit: ComposioToolkitSummaryDto) => {
|
||||
setBusySlug(toolkit.slug);
|
||||
setConnectError(null);
|
||||
void window.agentCompany
|
||||
.connectComposioToolkit({ orgId, toolkitSlug: toolkit.slug, toolkitName: toolkit.name })
|
||||
.then((result) => {
|
||||
if (!result.ok) throw new Error(result.error.message);
|
||||
if (result.data.redirectUrl) {
|
||||
void window.agentCompany.openExternalUrl(result.data.redirectUrl);
|
||||
.then(async (result) => {
|
||||
if (!result.ok) {
|
||||
setConnectError(result.error.message);
|
||||
return;
|
||||
}
|
||||
pollUntilActive(toolkit.slug, 0);
|
||||
if (result.data.redirectUrl) {
|
||||
const opened = await window.agentCompany.openExternalUrl(result.data.redirectUrl);
|
||||
if (!opened.ok) setConnectError(opened.error.message);
|
||||
}
|
||||
})
|
||||
.finally(() => setBusySlug(null));
|
||||
};
|
||||
@@ -112,6 +118,21 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
|
||||
<div style={{ maxWidth: 1060 }}>
|
||||
<h2 style={{ margin: "0 0 18px", fontSize: 16, fontWeight: 600 }}>Connectors</h2>
|
||||
|
||||
{connectError && (
|
||||
<div
|
||||
style={{
|
||||
color: "var(--text-2)",
|
||||
fontSize: 13,
|
||||
marginBottom: 14,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{connectError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConnectedList accounts={accounts} grants={grants} agents={agents} onGrant={grant} onRevoke={revoke} />
|
||||
|
||||
{catalog.phase === "loading" && (
|
||||
|
||||
@@ -21,7 +21,6 @@ export interface ConnectComposioToolkitInput {
|
||||
|
||||
export interface ConnectComposioToolkitResult {
|
||||
accountId: string;
|
||||
status: string;
|
||||
redirectUrl: string | null;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user