@agent-native/dispatch 0.13.12 → 0.14.0
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/dist/actions/delete-destination.d.ts +12 -12
- package/dist/actions/provider-api-audit.d.ts +7 -0
- package/dist/actions/provider-api-audit.d.ts.map +1 -0
- package/dist/actions/provider-api-audit.js +74 -0
- package/dist/actions/provider-api-audit.js.map +1 -0
- package/dist/actions/provider-api-catalog.d.ts +1 -1
- package/dist/actions/provider-api-request.js +10 -0
- package/dist/actions/provider-api-request.js.map +1 -1
- package/dist/actions/send-platform-message.d.ts +1 -0
- package/dist/actions/send-platform-message.js +43 -22
- package/dist/actions/send-platform-message.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +1 -1
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/messaging-setup-panel.d.ts.map +1 -1
- package/dist/components/messaging-setup-panel.js +203 -127
- package/dist/components/messaging-setup-panel.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +1 -2
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +3 -1
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +2 -2
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +1 -3
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/messaging.js +1 -1
- package/dist/routes/pages/messaging.js.map +1 -1
- package/dist/routes/pages/team.js +1 -1
- package/dist/routes/pages/team.js.map +1 -1
- package/dist/server/lib/dispatch-integrations.d.ts +7 -1
- package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
- package/dist/server/lib/dispatch-integrations.js +178 -8
- package/dist/server/lib/dispatch-integrations.js.map +1 -1
- package/dist/server/lib/dispatch-routing.d.ts +6 -0
- package/dist/server/lib/dispatch-routing.d.ts.map +1 -0
- package/dist/server/lib/dispatch-routing.js +30 -0
- package/dist/server/lib/dispatch-routing.js.map +1 -0
- package/dist/server/lib/env-config.d.ts.map +1 -1
- package/dist/server/lib/env-config.js +50 -0
- package/dist/server/lib/env-config.js.map +1 -1
- package/dist/server/lib/onboarding-steps.d.ts.map +1 -1
- package/dist/server/lib/onboarding-steps.js +25 -0
- package/dist/server/lib/onboarding-steps.js.map +1 -1
- package/dist/server/lib/provider-api.d.ts +2 -2
- package/dist/server/lib/provider-api.d.ts.map +1 -1
- package/dist/server/plugins/integrations.d.ts.map +1 -1
- package/dist/server/plugins/integrations.js +5 -2
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +2 -2
- package/src/actions/provider-api-audit.spec.ts +52 -0
- package/src/actions/provider-api-audit.ts +88 -0
- package/src/actions/provider-api-request.ts +10 -0
- package/src/actions/send-platform-message.spec.ts +108 -0
- package/src/actions/send-platform-message.ts +73 -25
- package/src/components/dispatch-control-plane.tsx +1 -5
- package/src/components/messaging-setup-panel.spec.tsx +191 -0
- package/src/components/messaging-setup-panel.tsx +472 -193
- package/src/hooks/use-navigation-state.ts +1 -2
- package/src/routes/pages/$appId.tsx +3 -5
- package/src/routes/pages/apps.$appId.tsx +3 -7
- package/src/routes/pages/apps.tsx +0 -3
- package/src/routes/pages/messaging.tsx +1 -1
- package/src/routes/pages/team.tsx +1 -1
- package/src/server/lib/dispatch-integrations.spec.ts +113 -0
- package/src/server/lib/dispatch-integrations.ts +239 -6
- package/src/server/lib/dispatch-routing.spec.ts +42 -0
- package/src/server/lib/dispatch-routing.ts +42 -0
- package/src/server/lib/env-config.ts +50 -0
- package/src/server/lib/onboarding-steps.ts +32 -0
- package/src/server/plugins/integrations.ts +5 -2
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import {
|
|
3
|
+
listIntegrationInstallations,
|
|
4
|
+
resolveIntegrationTokenBundle,
|
|
5
|
+
} from "@agent-native/core/integrations";
|
|
6
|
+
import {
|
|
7
|
+
getRequestOrgId,
|
|
8
|
+
getRequestUserEmail,
|
|
3
9
|
slackAdapter,
|
|
4
10
|
telegramAdapter,
|
|
5
11
|
emailAdapter,
|
|
@@ -8,21 +14,52 @@ import {
|
|
|
8
14
|
} from "@agent-native/core/server";
|
|
9
15
|
import { z } from "zod";
|
|
10
16
|
|
|
11
|
-
import {
|
|
12
|
-
getDestinationById,
|
|
13
|
-
recordAudit,
|
|
14
|
-
} from "../server/lib/dispatch-store.js";
|
|
17
|
+
import { getDestinationById } from "../server/lib/dispatch-store.js";
|
|
15
18
|
|
|
16
|
-
function getAdapter(
|
|
19
|
+
function getAdapter(
|
|
20
|
+
platform: "slack" | "telegram" | "email",
|
|
21
|
+
slackToken?: string,
|
|
22
|
+
) {
|
|
17
23
|
if (platform === "email") return emailAdapter();
|
|
18
|
-
return platform === "slack"
|
|
24
|
+
return platform === "slack"
|
|
25
|
+
? slackAdapter({ resolveBotToken: async () => slackToken })
|
|
26
|
+
: telegramAdapter();
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
async function assertOutboundConfigured(
|
|
22
30
|
platform: "slack" | "telegram" | "email",
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
tenantId?: string,
|
|
32
|
+
): Promise<string | undefined> {
|
|
33
|
+
if (platform === "slack") {
|
|
34
|
+
const userEmail = getRequestUserEmail();
|
|
35
|
+
if (!userEmail) throw new Error("An authenticated user is required");
|
|
36
|
+
const installations = tenantId
|
|
37
|
+
? await listIntegrationInstallations(
|
|
38
|
+
{ userEmail, orgId: getRequestOrgId() ?? null },
|
|
39
|
+
"slack",
|
|
40
|
+
)
|
|
41
|
+
: [];
|
|
42
|
+
const installation =
|
|
43
|
+
installations.find(
|
|
44
|
+
(candidate) =>
|
|
45
|
+
candidate.teamId === tenantId || candidate.enterpriseId === tenantId,
|
|
46
|
+
) ?? null;
|
|
47
|
+
const managed = installation
|
|
48
|
+
? await resolveIntegrationTokenBundle(
|
|
49
|
+
"slack",
|
|
50
|
+
installation.installationKey,
|
|
51
|
+
)
|
|
52
|
+
: null;
|
|
53
|
+
const token =
|
|
54
|
+
managed?.accessToken ?? (await resolveSecret("SLACK_BOT_TOKEN"));
|
|
55
|
+
if (!token) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
tenantId
|
|
58
|
+
? "That Slack workspace is not connected"
|
|
59
|
+
: "Select a Slack workspace for managed outbound messaging",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return token;
|
|
26
63
|
}
|
|
27
64
|
if (platform === "telegram" && !(await resolveSecret("TELEGRAM_BOT_TOKEN"))) {
|
|
28
65
|
throw new Error("Telegram outbound messaging is not configured");
|
|
@@ -45,9 +82,29 @@ export default defineAction({
|
|
|
45
82
|
destinationId: z.string().optional().describe("Saved destination id"),
|
|
46
83
|
destination: z.string().optional().describe("Raw platform destination id"),
|
|
47
84
|
threadRef: z.string().optional().describe("Optional thread reference"),
|
|
85
|
+
tenantId: z
|
|
86
|
+
.string()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe("Slack workspace/team id for managed installations"),
|
|
48
89
|
text: z.string().describe("Message to send"),
|
|
49
90
|
}),
|
|
50
|
-
|
|
91
|
+
audit: {
|
|
92
|
+
recordInputs: false,
|
|
93
|
+
target: (args) => ({
|
|
94
|
+
type: "destination",
|
|
95
|
+
id: args.destinationId || args.destination || "unknown",
|
|
96
|
+
visibility: "private",
|
|
97
|
+
}),
|
|
98
|
+
summary: (args) => `Sent proactive ${args.platform || "saved"} message`,
|
|
99
|
+
},
|
|
100
|
+
run: async ({
|
|
101
|
+
platform,
|
|
102
|
+
destinationId,
|
|
103
|
+
destination,
|
|
104
|
+
threadRef,
|
|
105
|
+
tenantId,
|
|
106
|
+
text,
|
|
107
|
+
}) => {
|
|
51
108
|
const saved = destinationId
|
|
52
109
|
? await getDestinationById(destinationId)
|
|
53
110
|
: null;
|
|
@@ -63,9 +120,12 @@ export default defineAction({
|
|
|
63
120
|
throw new Error("A platform and destination are required");
|
|
64
121
|
}
|
|
65
122
|
|
|
66
|
-
await assertOutboundConfigured(
|
|
123
|
+
const slackToken = await assertOutboundConfigured(
|
|
124
|
+
resolvedPlatform,
|
|
125
|
+
tenantId,
|
|
126
|
+
);
|
|
67
127
|
|
|
68
|
-
const adapter = getAdapter(resolvedPlatform);
|
|
128
|
+
const adapter = getAdapter(resolvedPlatform, slackToken);
|
|
69
129
|
if (!adapter.sendMessageToTarget) {
|
|
70
130
|
throw new Error(
|
|
71
131
|
`Platform ${resolvedPlatform} does not support proactive outbound messaging`,
|
|
@@ -76,19 +136,7 @@ export default defineAction({
|
|
|
76
136
|
destination: resolvedDestination,
|
|
77
137
|
threadRef: resolvedThreadRef,
|
|
78
138
|
label: saved?.name || undefined,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
await recordAudit({
|
|
82
|
-
action: "message.sent",
|
|
83
|
-
targetType: "destination",
|
|
84
|
-
targetId: destinationId || resolvedDestination,
|
|
85
|
-
summary: `Sent proactive ${resolvedPlatform} message${saved?.name ? ` to ${saved.name}` : ""}`,
|
|
86
|
-
metadata: {
|
|
87
|
-
platform: resolvedPlatform,
|
|
88
|
-
destination: resolvedDestination,
|
|
89
|
-
threadRef: resolvedThreadRef,
|
|
90
|
-
text,
|
|
91
|
-
},
|
|
139
|
+
tenantId,
|
|
92
140
|
});
|
|
93
141
|
|
|
94
142
|
return {
|
|
@@ -211,11 +211,7 @@ function AppsPanel({
|
|
|
211
211
|
export function DispatchControlPlane() {
|
|
212
212
|
const { data: workspaceApps = [], isLoading: appsLoading } = useActionQuery<
|
|
213
213
|
WorkspaceAppSummary[]
|
|
214
|
-
>(
|
|
215
|
-
"list-workspace-apps",
|
|
216
|
-
{ includeAgentCards: false, includeArchived: true },
|
|
217
|
-
{ refetchInterval: 2_000 },
|
|
218
|
-
);
|
|
214
|
+
>("list-workspace-apps", { includeAgentCards: false, includeArchived: true });
|
|
219
215
|
|
|
220
216
|
return (
|
|
221
217
|
<DispatchShell
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { MessagingSetupPanel } from "./messaging-setup-panel";
|
|
7
|
+
import { TooltipProvider } from "./ui/tooltip";
|
|
8
|
+
|
|
9
|
+
const clientState = vi.hoisted(() => ({
|
|
10
|
+
statuses: [] as any[],
|
|
11
|
+
envStatuses: [] as any[],
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("@agent-native/core/client", () => ({
|
|
15
|
+
disconnectManagedIntegrationInstallation: vi.fn(() => Promise.resolve()),
|
|
16
|
+
listManagedIntegrationBudgets: vi.fn(() => Promise.resolve([])),
|
|
17
|
+
listManagedIntegrationInstallations: vi.fn(() => Promise.resolve([])),
|
|
18
|
+
listManagedIntegrationScopes: vi.fn(() => Promise.resolve([])),
|
|
19
|
+
listIntegrationStatuses: vi.fn(() => Promise.resolve(clientState.statuses)),
|
|
20
|
+
listIntegrationEnvStatuses: vi.fn(() =>
|
|
21
|
+
Promise.resolve(clientState.envStatuses),
|
|
22
|
+
),
|
|
23
|
+
managedIntegrationOAuthUrl: vi.fn(
|
|
24
|
+
(platform: string) => `/_agent-native/integrations/${platform}/oauth/start`,
|
|
25
|
+
),
|
|
26
|
+
managedSlackAgentManifestUrl: vi.fn(
|
|
27
|
+
() => "/_agent-native/integrations/slack/manifest",
|
|
28
|
+
),
|
|
29
|
+
saveIntegrationEnvVars: vi.fn(),
|
|
30
|
+
saveManagedIntegrationBudget: vi.fn(() => Promise.resolve()),
|
|
31
|
+
saveManagedIntegrationScope: vi.fn(() => Promise.resolve()),
|
|
32
|
+
setIntegrationEnabled: vi.fn(),
|
|
33
|
+
setupIntegration: vi.fn(),
|
|
34
|
+
testManagedIntegrationInstallation: vi.fn(() => Promise.resolve()),
|
|
35
|
+
useFormatters: () => ({
|
|
36
|
+
formatDate: (value: Date | number | string) =>
|
|
37
|
+
new Date(value).toLocaleDateString("en-US"),
|
|
38
|
+
}),
|
|
39
|
+
useT: () => (key: string) =>
|
|
40
|
+
(
|
|
41
|
+
({
|
|
42
|
+
"messaging.managed.agentManifest": "Agent manifest",
|
|
43
|
+
"messaging.managed.agentManifestDescription":
|
|
44
|
+
"The Agent manifest enables Slack's Agent view and direct messages.",
|
|
45
|
+
"messaging.managed.addToSlack": "Add to Slack",
|
|
46
|
+
"messaging.managed.requiredCredentials":
|
|
47
|
+
"Save the required Slack app credentials below to enable Add to Slack.",
|
|
48
|
+
}) as Record<string, string>
|
|
49
|
+
)[key] ?? key,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
vi.mock("@agent-native/core/integrations", () => ({
|
|
53
|
+
listBuiltInChannelIntegrations: () => [
|
|
54
|
+
{
|
|
55
|
+
id: "slack",
|
|
56
|
+
name: "Slack",
|
|
57
|
+
iconKey: "slack",
|
|
58
|
+
description: "Slack description",
|
|
59
|
+
documentation: { href: "/docs/messaging#slack" },
|
|
60
|
+
setup: { steps: ["Create a Slack app."] },
|
|
61
|
+
credentialRequirements: [
|
|
62
|
+
{ key: "SLACK_BOT_TOKEN", label: "Slack Bot Token", required: true },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "email",
|
|
67
|
+
name: "Email",
|
|
68
|
+
iconKey: "email",
|
|
69
|
+
description: "Email description",
|
|
70
|
+
documentation: { href: "/docs/messaging#email" },
|
|
71
|
+
setup: { steps: ["Choose a provider."] },
|
|
72
|
+
credentialRequirements: [
|
|
73
|
+
{
|
|
74
|
+
key: "RESEND_API_KEY",
|
|
75
|
+
label: "Resend API Key",
|
|
76
|
+
required: true,
|
|
77
|
+
alternativeGroup: "email-provider",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: "SENDGRID_API_KEY",
|
|
81
|
+
label: "SendGrid API Key",
|
|
82
|
+
required: true,
|
|
83
|
+
alternativeGroup: "email-provider",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
describe("MessagingSetupPanel", () => {
|
|
91
|
+
let container: HTMLDivElement;
|
|
92
|
+
let root: Root;
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
96
|
+
clientState.statuses = [];
|
|
97
|
+
clientState.envStatuses = [];
|
|
98
|
+
container = document.createElement("div");
|
|
99
|
+
document.body.appendChild(container);
|
|
100
|
+
root = createRoot(container);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
act(() => root.unmount());
|
|
105
|
+
container.remove();
|
|
106
|
+
vi.unstubAllGlobals();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("renders catalog-backed channel cards", async () => {
|
|
110
|
+
await act(async () => {
|
|
111
|
+
root.render(
|
|
112
|
+
<TooltipProvider>
|
|
113
|
+
<MessagingSetupPanel />
|
|
114
|
+
</TooltipProvider>,
|
|
115
|
+
);
|
|
116
|
+
await Promise.resolve();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(container.textContent).toContain("Slack");
|
|
120
|
+
expect(container.textContent).toContain("Slack description");
|
|
121
|
+
expect(container.textContent).toContain("Email");
|
|
122
|
+
expect(container.textContent).toContain("Email description");
|
|
123
|
+
expect(container.textContent).not.toContain("Discord");
|
|
124
|
+
expect(
|
|
125
|
+
container.querySelector(
|
|
126
|
+
'a[href="/_agent-native/integrations/slack/manifest"]',
|
|
127
|
+
)?.textContent,
|
|
128
|
+
).toContain("Agent manifest");
|
|
129
|
+
expect(container.textContent).toContain(
|
|
130
|
+
"enables Slack's Agent view and direct messages",
|
|
131
|
+
);
|
|
132
|
+
expect(
|
|
133
|
+
container.querySelector(
|
|
134
|
+
'a[href="/_agent-native/integrations/slack/oauth/start"]',
|
|
135
|
+
),
|
|
136
|
+
).toBeNull();
|
|
137
|
+
expect(
|
|
138
|
+
[...container.querySelectorAll("button")].find((button) =>
|
|
139
|
+
button.textContent?.includes("Add to Slack"),
|
|
140
|
+
)?.disabled,
|
|
141
|
+
).toBe(true);
|
|
142
|
+
expect(container.textContent).toContain(
|
|
143
|
+
"Save the required Slack app credentials below",
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("shows connected and alternative credential states", async () => {
|
|
148
|
+
clientState.statuses = [
|
|
149
|
+
{ platform: "slack", label: "Slack", configured: true, enabled: true },
|
|
150
|
+
];
|
|
151
|
+
clientState.envStatuses = [
|
|
152
|
+
{
|
|
153
|
+
key: "SLACK_BOT_TOKEN",
|
|
154
|
+
label: "Slack Bot Token",
|
|
155
|
+
required: true,
|
|
156
|
+
configured: true,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
key: "RESEND_API_KEY",
|
|
160
|
+
label: "Resend API Key",
|
|
161
|
+
required: true,
|
|
162
|
+
configured: true,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
key: "SENDGRID_API_KEY",
|
|
166
|
+
label: "SendGrid API Key",
|
|
167
|
+
required: true,
|
|
168
|
+
configured: false,
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
await act(async () => {
|
|
173
|
+
root.render(
|
|
174
|
+
<TooltipProvider>
|
|
175
|
+
<MessagingSetupPanel />
|
|
176
|
+
</TooltipProvider>,
|
|
177
|
+
);
|
|
178
|
+
await Promise.resolve();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(container.textContent).toContain("Connected");
|
|
182
|
+
expect(container.textContent).toContain("Saved");
|
|
183
|
+
expect(
|
|
184
|
+
container.querySelector(
|
|
185
|
+
'a[href="/_agent-native/integrations/slack/oauth/start"]',
|
|
186
|
+
),
|
|
187
|
+
).not.toBeNull();
|
|
188
|
+
expect(container.querySelectorAll("button").length).toBeGreaterThan(0);
|
|
189
|
+
expect(container.textContent).not.toContain("Save credentials");
|
|
190
|
+
});
|
|
191
|
+
});
|