@frockbot/plugin-composio 0.0.0 → 0.1.1
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/frockbot.json +34 -0
- package/package.json +37 -6
- package/src/agent.test.ts +207 -0
- package/src/agent.ts +227 -0
- package/src/backend-contracts.ts +8 -0
- package/src/backend.test.ts +333 -0
- package/src/backend.ts +385 -0
- package/src/composio-client.test.ts +208 -0
- package/src/composio-client.ts +232 -0
- package/src/connection-recovery.test.ts +54 -0
- package/src/connection-recovery.ts +52 -0
- package/src/connections.ts +861 -0
- package/src/dependency-coordination.test.ts +136 -0
- package/src/dependency-coordination.ts +140 -0
- package/src/index.ts +7 -0
- package/src/manifest.ts +3 -0
- package/src/provider-reconciliation.ts +71 -0
- package/src/user-configuration.test.ts +1937 -0
- package/src/user-configuration.ts +1366 -0
- package/tsconfig.json +15 -0
- package/README.md +0 -3
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ConnectionView } from "@frockbot/configuration-core";
|
|
3
|
+
import {
|
|
4
|
+
acknowledgeDependentAssignment,
|
|
5
|
+
claimDependentAssignment,
|
|
6
|
+
compensateDependentAssignment,
|
|
7
|
+
releaseDependentAssignment,
|
|
8
|
+
} from "./dependency-coordination.js";
|
|
9
|
+
|
|
10
|
+
function assignmentConnection(): ConnectionView {
|
|
11
|
+
return {
|
|
12
|
+
connectionId: "connection-1",
|
|
13
|
+
packageId: "composio",
|
|
14
|
+
connectionTypeId: "gmail",
|
|
15
|
+
displayName: "Gmail",
|
|
16
|
+
state: "ready",
|
|
17
|
+
safeMetadata: {},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("Connection dependency coordination", () => {
|
|
22
|
+
test("records intent before acknowledgement and rejects revocation races", () => {
|
|
23
|
+
const claimed = claimDependentAssignment(
|
|
24
|
+
assignmentConnection(),
|
|
25
|
+
"bot-1",
|
|
26
|
+
"generation-1",
|
|
27
|
+
);
|
|
28
|
+
expect(claimed?.safeMetadata.dependentAssignments).toEqual([
|
|
29
|
+
{ botId: "bot-1", generation: "generation-1", status: "pending" },
|
|
30
|
+
]);
|
|
31
|
+
expect(
|
|
32
|
+
acknowledgeDependentAssignment(claimed!, "bot-1", "generation-1")
|
|
33
|
+
?.safeMetadata.dependentAssignments,
|
|
34
|
+
).toEqual([
|
|
35
|
+
{
|
|
36
|
+
botId: "bot-1",
|
|
37
|
+
generation: "generation-1",
|
|
38
|
+
status: "acknowledged",
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
41
|
+
expect(
|
|
42
|
+
claimDependentAssignment(
|
|
43
|
+
{
|
|
44
|
+
...assignmentConnection(),
|
|
45
|
+
state: "revoking",
|
|
46
|
+
safeMetadata: {
|
|
47
|
+
...assignmentConnection().safeMetadata,
|
|
48
|
+
revocationRequested: true,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
"bot-1",
|
|
52
|
+
"generation-1",
|
|
53
|
+
),
|
|
54
|
+
).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("keeps two same-Bot Assignment generations independent on one Connection", () => {
|
|
58
|
+
const acknowledged = {
|
|
59
|
+
...assignmentConnection(),
|
|
60
|
+
state: "ready" as const,
|
|
61
|
+
safeMetadata: {
|
|
62
|
+
dependentAssignments: [
|
|
63
|
+
{
|
|
64
|
+
botId: "bot-1",
|
|
65
|
+
generation: "generation-1",
|
|
66
|
+
status: "acknowledged",
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const claimed = claimDependentAssignment(
|
|
72
|
+
acknowledged,
|
|
73
|
+
"bot-1",
|
|
74
|
+
"generation-2",
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(claimed?.safeMetadata.dependentAssignments).toEqual([
|
|
78
|
+
{
|
|
79
|
+
botId: "bot-1",
|
|
80
|
+
generation: "generation-1",
|
|
81
|
+
status: "acknowledged",
|
|
82
|
+
},
|
|
83
|
+
{ botId: "bot-1", generation: "generation-2", status: "pending" },
|
|
84
|
+
]);
|
|
85
|
+
expect(
|
|
86
|
+
compensateDependentAssignment(claimed!, "bot-1", "generation-2")
|
|
87
|
+
?.safeMetadata.dependentAssignments,
|
|
88
|
+
).toEqual([
|
|
89
|
+
{
|
|
90
|
+
botId: "bot-1",
|
|
91
|
+
generation: "generation-1",
|
|
92
|
+
status: "acknowledged",
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
const bothAcknowledged = acknowledgeDependentAssignment(
|
|
96
|
+
claimed!,
|
|
97
|
+
"bot-1",
|
|
98
|
+
"generation-2",
|
|
99
|
+
);
|
|
100
|
+
expect(bothAcknowledged?.safeMetadata.dependentAssignments).toEqual([
|
|
101
|
+
{
|
|
102
|
+
botId: "bot-1",
|
|
103
|
+
generation: "generation-1",
|
|
104
|
+
status: "acknowledged",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
botId: "bot-1",
|
|
108
|
+
generation: "generation-2",
|
|
109
|
+
status: "acknowledged",
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const firstReleased = releaseDependentAssignment(
|
|
114
|
+
bothAcknowledged!,
|
|
115
|
+
"bot-1",
|
|
116
|
+
"generation-1",
|
|
117
|
+
);
|
|
118
|
+
expect(firstReleased?.safeMetadata.dependentAssignments).toEqual([
|
|
119
|
+
{
|
|
120
|
+
botId: "bot-1",
|
|
121
|
+
generation: "generation-2",
|
|
122
|
+
status: "acknowledged",
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
125
|
+
expect(
|
|
126
|
+
releaseDependentAssignment(bothAcknowledged!, "bot-1", "generation-2")
|
|
127
|
+
?.safeMetadata.dependentAssignments,
|
|
128
|
+
).toEqual([
|
|
129
|
+
{
|
|
130
|
+
botId: "bot-1",
|
|
131
|
+
generation: "generation-1",
|
|
132
|
+
status: "acknowledged",
|
|
133
|
+
},
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { ConnectionView } from "@frockbot/configuration-core";
|
|
2
|
+
|
|
3
|
+
export function claimDependentAssignment(
|
|
4
|
+
connection: ConnectionView,
|
|
5
|
+
botId: string,
|
|
6
|
+
generation: string,
|
|
7
|
+
): ConnectionView | undefined {
|
|
8
|
+
if (
|
|
9
|
+
connection.state === "revoking" ||
|
|
10
|
+
connection.state === "revoked" ||
|
|
11
|
+
connection.state === "failed" ||
|
|
12
|
+
connection.safeMetadata.revocationRequested === true
|
|
13
|
+
) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
if (connection.state !== "ready") {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const existing = Array.isArray(connection.safeMetadata.dependentAssignments)
|
|
20
|
+
? connection.safeMetadata.dependentAssignments.filter(
|
|
21
|
+
(candidate) =>
|
|
22
|
+
candidate &&
|
|
23
|
+
typeof candidate === "object" &&
|
|
24
|
+
!Array.isArray(candidate) &&
|
|
25
|
+
typeof (candidate as Record<string, unknown>).botId === "string",
|
|
26
|
+
)
|
|
27
|
+
: [];
|
|
28
|
+
return {
|
|
29
|
+
...connection,
|
|
30
|
+
safeMetadata: {
|
|
31
|
+
...connection.safeMetadata,
|
|
32
|
+
dependentAssignments: [
|
|
33
|
+
...existing.filter(
|
|
34
|
+
(candidate) =>
|
|
35
|
+
(candidate as Record<string, unknown>).botId !== botId ||
|
|
36
|
+
(candidate as Record<string, unknown>).generation !== generation,
|
|
37
|
+
),
|
|
38
|
+
{ botId, generation, status: "pending" },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function acknowledgeDependentAssignment(
|
|
45
|
+
connection: ConnectionView,
|
|
46
|
+
botId: string,
|
|
47
|
+
generation: string,
|
|
48
|
+
): ConnectionView | undefined {
|
|
49
|
+
if (
|
|
50
|
+
connection.state === "revoking" ||
|
|
51
|
+
connection.state === "revoked" ||
|
|
52
|
+
connection.safeMetadata.revocationRequested === true
|
|
53
|
+
) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const dependencies = Array.isArray(
|
|
57
|
+
connection.safeMetadata.dependentAssignments,
|
|
58
|
+
)
|
|
59
|
+
? connection.safeMetadata.dependentAssignments
|
|
60
|
+
: [];
|
|
61
|
+
let matched = false;
|
|
62
|
+
const acknowledged = dependencies.map((candidate) => {
|
|
63
|
+
if (
|
|
64
|
+
candidate &&
|
|
65
|
+
typeof candidate === "object" &&
|
|
66
|
+
!Array.isArray(candidate) &&
|
|
67
|
+
(candidate as Record<string, unknown>).botId === botId &&
|
|
68
|
+
(candidate as Record<string, unknown>).generation === generation
|
|
69
|
+
) {
|
|
70
|
+
matched = true;
|
|
71
|
+
return { ...candidate, status: "acknowledged" };
|
|
72
|
+
}
|
|
73
|
+
return candidate;
|
|
74
|
+
});
|
|
75
|
+
if (!matched) return undefined;
|
|
76
|
+
return {
|
|
77
|
+
...connection,
|
|
78
|
+
safeMetadata: {
|
|
79
|
+
...connection.safeMetadata,
|
|
80
|
+
dependentAssignments: acknowledged,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function releaseDependentAssignment(
|
|
86
|
+
connection: ConnectionView,
|
|
87
|
+
botId: string,
|
|
88
|
+
generation: string,
|
|
89
|
+
): ConnectionView | undefined {
|
|
90
|
+
const dependencies = Array.isArray(
|
|
91
|
+
connection.safeMetadata.dependentAssignments,
|
|
92
|
+
)
|
|
93
|
+
? connection.safeMetadata.dependentAssignments
|
|
94
|
+
: [];
|
|
95
|
+
const remaining = dependencies.filter(
|
|
96
|
+
(candidate) =>
|
|
97
|
+
!candidate ||
|
|
98
|
+
typeof candidate !== "object" ||
|
|
99
|
+
Array.isArray(candidate) ||
|
|
100
|
+
(candidate as Record<string, unknown>).botId !== botId ||
|
|
101
|
+
(candidate as Record<string, unknown>).generation !== generation,
|
|
102
|
+
);
|
|
103
|
+
if (remaining.length === dependencies.length) return undefined;
|
|
104
|
+
return {
|
|
105
|
+
...connection,
|
|
106
|
+
safeMetadata: {
|
|
107
|
+
...connection.safeMetadata,
|
|
108
|
+
dependentAssignments: remaining,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function compensateDependentAssignment(
|
|
114
|
+
connection: ConnectionView,
|
|
115
|
+
botId: string,
|
|
116
|
+
generation: string,
|
|
117
|
+
): ConnectionView | undefined {
|
|
118
|
+
const dependencies = Array.isArray(
|
|
119
|
+
connection.safeMetadata.dependentAssignments,
|
|
120
|
+
)
|
|
121
|
+
? connection.safeMetadata.dependentAssignments
|
|
122
|
+
: [];
|
|
123
|
+
const remaining = dependencies.filter(
|
|
124
|
+
(candidate) =>
|
|
125
|
+
!candidate ||
|
|
126
|
+
typeof candidate !== "object" ||
|
|
127
|
+
Array.isArray(candidate) ||
|
|
128
|
+
(candidate as Record<string, unknown>).botId !== botId ||
|
|
129
|
+
(candidate as Record<string, unknown>).generation !== generation ||
|
|
130
|
+
(candidate as Record<string, unknown>).status !== "pending",
|
|
131
|
+
);
|
|
132
|
+
if (remaining.length === dependencies.length) return undefined;
|
|
133
|
+
return {
|
|
134
|
+
...connection,
|
|
135
|
+
safeMetadata: {
|
|
136
|
+
...connection.safeMetadata,
|
|
137
|
+
dependentAssignments: remaining,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./agent.js";
|
|
2
|
+
export * from "./backend.js";
|
|
3
|
+
export * from "./composio-client.js";
|
|
4
|
+
export * from "./connection-recovery.js";
|
|
5
|
+
export * from "./connections.js";
|
|
6
|
+
export * from "./dependency-coordination.js";
|
|
7
|
+
export * from "./provider-reconciliation.js";
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ComposioRequestError,
|
|
3
|
+
type ComposioClient,
|
|
4
|
+
type ConnectedAccountSummary,
|
|
5
|
+
} from "./composio-client.js";
|
|
6
|
+
|
|
7
|
+
export type ComposioProviderReconciliationRequest =
|
|
8
|
+
| {
|
|
9
|
+
operation: "link";
|
|
10
|
+
userId: string;
|
|
11
|
+
providerAlias: string;
|
|
12
|
+
toolkitSlug: string;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
operation: "revoke";
|
|
16
|
+
userId: string;
|
|
17
|
+
connectedAccountId: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ComposioProviderReconciliationResult =
|
|
21
|
+
| { status: "active"; account: ConnectedAccountSummary }
|
|
22
|
+
| { status: "failed"; account: ConnectedAccountSummary }
|
|
23
|
+
| { status: "revoked"; account?: ConnectedAccountSummary }
|
|
24
|
+
| { status: "absent" }
|
|
25
|
+
| { status: "pending"; account?: ConnectedAccountSummary };
|
|
26
|
+
|
|
27
|
+
export function linkReconciliationDisposition(
|
|
28
|
+
result: ComposioProviderReconciliationResult,
|
|
29
|
+
): "ready" | "pending" | "failed" {
|
|
30
|
+
if (result.status === "active") return "ready";
|
|
31
|
+
if (result.status === "pending" || result.status === "absent") {
|
|
32
|
+
return "pending";
|
|
33
|
+
}
|
|
34
|
+
return "failed";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function reconcileComposioProviderConnection(
|
|
38
|
+
client: ComposioClient,
|
|
39
|
+
request: ComposioProviderReconciliationRequest,
|
|
40
|
+
): Promise<ComposioProviderReconciliationResult> {
|
|
41
|
+
if (request.operation === "link") {
|
|
42
|
+
const account = (await client.listConnectedAccounts(request.userId)).find(
|
|
43
|
+
(candidate) =>
|
|
44
|
+
candidate.alias === request.providerAlias &&
|
|
45
|
+
candidate.toolkitSlug === request.toolkitSlug,
|
|
46
|
+
);
|
|
47
|
+
if (!account) return { status: "absent" };
|
|
48
|
+
if (account.status === "ACTIVE") return { status: "active", account };
|
|
49
|
+
if (account.status === "REVOKED") return { status: "revoked", account };
|
|
50
|
+
if (account.status === "FAILED" || account.status === "EXPIRED") {
|
|
51
|
+
return { status: "failed", account };
|
|
52
|
+
}
|
|
53
|
+
return { status: "pending", account };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let account: ConnectedAccountSummary;
|
|
57
|
+
try {
|
|
58
|
+
account = await client.getConnectedAccount(request.connectedAccountId);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof ComposioRequestError && error.status === 404) {
|
|
61
|
+
return { status: "absent" };
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
if (account.userId !== request.userId) {
|
|
66
|
+
throw new Error("Composio connected account does not belong to the User");
|
|
67
|
+
}
|
|
68
|
+
return account.status === "REVOKED"
|
|
69
|
+
? { status: "revoked", account }
|
|
70
|
+
: { status: "pending", account };
|
|
71
|
+
}
|