@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 +6 -6
- package/src/backend.test.ts +32 -0
- package/src/backend.ts +45 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-composio",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
26
|
-
"@frockbot/connection-core": "0.3.
|
|
27
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
28
|
-
"@frockbot/plugin-settings": "0.3.
|
|
29
|
-
"@frockbot/plugin-tools": "0.3.
|
|
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": {
|
package/src/backend.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
206
|
-
|
|
207
|
-
error instanceof Error
|
|
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
|
|
336
|
-
|
|
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
|
|
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
|
|
358
|
-
|
|
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
|
},
|