@frockbot/plugin-composio 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-composio",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -22,11 +22,11 @@
22
22
  "typecheck": "tsc --noEmit -p tsconfig.json"
23
23
  },
24
24
  "dependencies": {
25
- "@frockbot/configuration-core": "0.3.6",
26
- "@frockbot/connection-core": "0.3.6",
27
- "@frockbot/kernel-contracts": "0.3.6",
28
- "@frockbot/plugin-settings": "0.3.6",
29
- "@frockbot/plugin-tools": "0.3.6",
25
+ "@frockbot/configuration-core": "0.3.7",
26
+ "@frockbot/connection-core": "0.3.7",
27
+ "@frockbot/kernel-contracts": "0.3.7",
28
+ "@frockbot/plugin-settings": "0.3.7",
29
+ "@frockbot/plugin-tools": "0.3.7",
30
30
  "cordis": "4.0.0-rc.8"
31
31
  },
32
32
  "devDependencies": {
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import type { ConnectionView } from "@frockbot/configuration-core";
3
3
  import {
4
+ callbackFailureResponse,
4
5
  connectionCompletionResponse,
5
6
  createComposioBackendContribution,
6
7
  createConfiguredComposioBackendContribution,
@@ -130,6 +131,37 @@ describe("Composio authorization return handoff", () => {
130
131
  });
131
132
  });
132
133
 
134
+ describe("Composio callback failures", () => {
135
+ test("returns the browser to the app with the reason instead of raw JSON", () => {
136
+ const response = callbackFailureResponse(
137
+ new URL("https://bot.frockbot.com/api/plugins/composio/callback"),
138
+ "authorization state has expired",
139
+ );
140
+
141
+ expect(response.status).toBe(303);
142
+ const location = new URL(response.headers.get("location") ?? "");
143
+ expect(location.origin).toBe("https://bot.frockbot.com");
144
+ expect(location.pathname).toBe("/");
145
+ expect(location.searchParams.get("connection")).toBe("composio-failed");
146
+ expect(location.searchParams.get("connection_reason")).toBe(
147
+ "authorization state has expired",
148
+ );
149
+ });
150
+
151
+ test("keeps a JSON body for the desktop target", async () => {
152
+ const response = callbackFailureResponse(
153
+ new URL("https://bot.frockbot.com/api/plugins/composio/callback"),
154
+ "authorization state has expired",
155
+ "desktop",
156
+ );
157
+
158
+ expect(response.status).toBe(400);
159
+ await expect(response.json()).resolves.toMatchObject({
160
+ error: "authorization state has expired",
161
+ });
162
+ });
163
+ });
164
+
133
165
  describe("Composio revoke route", () => {
134
166
  test("rejects invalid Connection identifiers before resolving storage", async () => {
135
167
  let storeLookups = 0;
package/src/backend.ts CHANGED
@@ -151,6 +151,29 @@ function coordinator(
151
151
  });
152
152
  }
153
153
 
154
+ /**
155
+ * Answer a failed *browser* callback by returning the User to the app with the
156
+ * reason attached, not with a JSON body rendered as a page.
157
+ *
158
+ * The callback is a top-level navigation, so `{"error":"..."}` is what the User
159
+ * sees, on a URL with no way back into the app. A slow consent screen is enough
160
+ * to land here — the authorization state is only valid for ten minutes.
161
+ */
162
+ export function callbackFailureResponse(
163
+ url: URL,
164
+ message: string,
165
+ target: "browser" | "desktop" = "browser",
166
+ ): Response {
167
+ if (target === "desktop") return jsonError(400, message);
168
+ const destination = new URL("/", url.origin);
169
+ destination.searchParams.set("connection", "composio-failed");
170
+ destination.searchParams.set("connection_reason", message.slice(0, 300));
171
+ return new Response(null, {
172
+ status: 303,
173
+ headers: { location: destination.toString() },
174
+ });
175
+ }
176
+
154
177
  export function connectionCompletionResponse(
155
178
  url: URL,
156
179
  target: "browser" | "desktop",
@@ -194,17 +217,23 @@ export function createComposioBackendContribution(
194
217
  let callbackState: AuthorizationState | undefined;
195
218
  if (isCallback) {
196
219
  const encodedState = url.searchParams.get("state");
197
- if (!encodedState)
198
- return jsonError(400, "Composio callback state is required");
220
+ if (!encodedState) {
221
+ return callbackFailureResponse(
222
+ url,
223
+ "This connection link is missing its authorization state. Start the connection again.",
224
+ );
225
+ }
199
226
  try {
200
227
  callbackState = await decodeAuthorizationState(
201
228
  encodedState,
202
229
  config.authorizationStateSecret,
203
230
  );
204
231
  } catch (error) {
205
- return jsonError(
206
- 400,
207
- error instanceof Error ? error.message : "Connection failed",
232
+ return callbackFailureResponse(
233
+ url,
234
+ error instanceof Error
235
+ ? error.message
236
+ : "This connection link is no longer valid. Start the connection again.",
208
237
  );
209
238
  }
210
239
  }
@@ -332,14 +361,19 @@ export function createComposioBackendContribution(
332
361
  result.nativeReturnNonce,
333
362
  );
334
363
  } catch (error) {
335
- return jsonError(
336
- 500,
364
+ return callbackFailureResponse(
365
+ url,
337
366
  error instanceof Error ? error.message : "Connection failed",
367
+ callbackState?.returnTarget === "desktop" ? "desktop" : "browser",
338
368
  );
339
369
  }
340
370
  }
341
371
  if (!connectionId || !connectedAccountId) {
342
- return jsonError(400, "Composio callback is incomplete");
372
+ return callbackFailureResponse(
373
+ url,
374
+ "The provider did not return a connected account. Start the connection again.",
375
+ callbackState?.returnTarget === "desktop" ? "desktop" : "browser",
376
+ );
343
377
  }
344
378
  try {
345
379
  const result = await connections.complete(user, {
@@ -354,9 +388,10 @@ export function createComposioBackendContribution(
354
388
  result.nativeReturnNonce,
355
389
  );
356
390
  } catch (error) {
357
- return jsonError(
358
- 400,
391
+ return callbackFailureResponse(
392
+ url,
359
393
  error instanceof Error ? error.message : "Connection failed",
394
+ callbackState?.returnTarget === "desktop" ? "desktop" : "browser",
360
395
  );
361
396
  }
362
397
  },