@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.
Files changed (74) hide show
  1. package/dist/actions/delete-destination.d.ts +12 -12
  2. package/dist/actions/provider-api-audit.d.ts +7 -0
  3. package/dist/actions/provider-api-audit.d.ts.map +1 -0
  4. package/dist/actions/provider-api-audit.js +74 -0
  5. package/dist/actions/provider-api-audit.js.map +1 -0
  6. package/dist/actions/provider-api-catalog.d.ts +1 -1
  7. package/dist/actions/provider-api-request.js +10 -0
  8. package/dist/actions/provider-api-request.js.map +1 -1
  9. package/dist/actions/send-platform-message.d.ts +1 -0
  10. package/dist/actions/send-platform-message.js +43 -22
  11. package/dist/actions/send-platform-message.js.map +1 -1
  12. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  13. package/dist/components/dispatch-control-plane.js +1 -1
  14. package/dist/components/dispatch-control-plane.js.map +1 -1
  15. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  16. package/dist/components/messaging-setup-panel.js +203 -127
  17. package/dist/components/messaging-setup-panel.js.map +1 -1
  18. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  19. package/dist/hooks/use-navigation-state.js +1 -2
  20. package/dist/hooks/use-navigation-state.js.map +1 -1
  21. package/dist/routes/pages/$appId.d.ts.map +1 -1
  22. package/dist/routes/pages/$appId.js +3 -1
  23. package/dist/routes/pages/$appId.js.map +1 -1
  24. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  25. package/dist/routes/pages/apps.$appId.js +2 -2
  26. package/dist/routes/pages/apps.$appId.js.map +1 -1
  27. package/dist/routes/pages/apps.d.ts.map +1 -1
  28. package/dist/routes/pages/apps.js +1 -3
  29. package/dist/routes/pages/apps.js.map +1 -1
  30. package/dist/routes/pages/messaging.js +1 -1
  31. package/dist/routes/pages/messaging.js.map +1 -1
  32. package/dist/routes/pages/team.js +1 -1
  33. package/dist/routes/pages/team.js.map +1 -1
  34. package/dist/server/lib/dispatch-integrations.d.ts +7 -1
  35. package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
  36. package/dist/server/lib/dispatch-integrations.js +178 -8
  37. package/dist/server/lib/dispatch-integrations.js.map +1 -1
  38. package/dist/server/lib/dispatch-routing.d.ts +6 -0
  39. package/dist/server/lib/dispatch-routing.d.ts.map +1 -0
  40. package/dist/server/lib/dispatch-routing.js +30 -0
  41. package/dist/server/lib/dispatch-routing.js.map +1 -0
  42. package/dist/server/lib/env-config.d.ts.map +1 -1
  43. package/dist/server/lib/env-config.js +50 -0
  44. package/dist/server/lib/env-config.js.map +1 -1
  45. package/dist/server/lib/onboarding-steps.d.ts.map +1 -1
  46. package/dist/server/lib/onboarding-steps.js +25 -0
  47. package/dist/server/lib/onboarding-steps.js.map +1 -1
  48. package/dist/server/lib/provider-api.d.ts +2 -2
  49. package/dist/server/lib/provider-api.d.ts.map +1 -1
  50. package/dist/server/plugins/integrations.d.ts.map +1 -1
  51. package/dist/server/plugins/integrations.js +5 -2
  52. package/dist/server/plugins/integrations.js.map +1 -1
  53. package/package.json +2 -2
  54. package/src/actions/provider-api-audit.spec.ts +52 -0
  55. package/src/actions/provider-api-audit.ts +88 -0
  56. package/src/actions/provider-api-request.ts +10 -0
  57. package/src/actions/send-platform-message.spec.ts +108 -0
  58. package/src/actions/send-platform-message.ts +73 -25
  59. package/src/components/dispatch-control-plane.tsx +1 -5
  60. package/src/components/messaging-setup-panel.spec.tsx +191 -0
  61. package/src/components/messaging-setup-panel.tsx +472 -193
  62. package/src/hooks/use-navigation-state.ts +1 -2
  63. package/src/routes/pages/$appId.tsx +3 -5
  64. package/src/routes/pages/apps.$appId.tsx +3 -7
  65. package/src/routes/pages/apps.tsx +0 -3
  66. package/src/routes/pages/messaging.tsx +1 -1
  67. package/src/routes/pages/team.tsx +1 -1
  68. package/src/server/lib/dispatch-integrations.spec.ts +113 -0
  69. package/src/server/lib/dispatch-integrations.ts +239 -6
  70. package/src/server/lib/dispatch-routing.spec.ts +42 -0
  71. package/src/server/lib/dispatch-routing.ts +42 -0
  72. package/src/server/lib/env-config.ts +50 -0
  73. package/src/server/lib/onboarding-steps.ts +32 -0
  74. 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(platform: "slack" | "telegram" | "email") {
19
+ function getAdapter(
20
+ platform: "slack" | "telegram" | "email",
21
+ slackToken?: string,
22
+ ) {
17
23
  if (platform === "email") return emailAdapter();
18
- return platform === "slack" ? slackAdapter() : telegramAdapter();
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
- if (platform === "slack" && !(await resolveSecret("SLACK_BOT_TOKEN"))) {
25
- throw new Error("Slack outbound messaging is not configured");
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
- run: async ({ platform, destinationId, destination, threadRef, text }) => {
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(resolvedPlatform);
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
+ });