@agent-native/dispatch 0.13.13 → 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 (51) 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/messaging-setup-panel.d.ts.map +1 -1
  13. package/dist/components/messaging-setup-panel.js +203 -127
  14. package/dist/components/messaging-setup-panel.js.map +1 -1
  15. package/dist/routes/pages/messaging.js +1 -1
  16. package/dist/routes/pages/messaging.js.map +1 -1
  17. package/dist/server/lib/dispatch-integrations.d.ts +7 -1
  18. package/dist/server/lib/dispatch-integrations.d.ts.map +1 -1
  19. package/dist/server/lib/dispatch-integrations.js +178 -8
  20. package/dist/server/lib/dispatch-integrations.js.map +1 -1
  21. package/dist/server/lib/dispatch-routing.d.ts +6 -0
  22. package/dist/server/lib/dispatch-routing.d.ts.map +1 -0
  23. package/dist/server/lib/dispatch-routing.js +30 -0
  24. package/dist/server/lib/dispatch-routing.js.map +1 -0
  25. package/dist/server/lib/env-config.d.ts.map +1 -1
  26. package/dist/server/lib/env-config.js +50 -0
  27. package/dist/server/lib/env-config.js.map +1 -1
  28. package/dist/server/lib/onboarding-steps.d.ts.map +1 -1
  29. package/dist/server/lib/onboarding-steps.js +25 -0
  30. package/dist/server/lib/onboarding-steps.js.map +1 -1
  31. package/dist/server/lib/provider-api.d.ts +2 -2
  32. package/dist/server/lib/provider-api.d.ts.map +1 -1
  33. package/dist/server/plugins/integrations.d.ts.map +1 -1
  34. package/dist/server/plugins/integrations.js +5 -2
  35. package/dist/server/plugins/integrations.js.map +1 -1
  36. package/package.json +2 -2
  37. package/src/actions/provider-api-audit.spec.ts +52 -0
  38. package/src/actions/provider-api-audit.ts +88 -0
  39. package/src/actions/provider-api-request.ts +10 -0
  40. package/src/actions/send-platform-message.spec.ts +108 -0
  41. package/src/actions/send-platform-message.ts +73 -25
  42. package/src/components/messaging-setup-panel.spec.tsx +191 -0
  43. package/src/components/messaging-setup-panel.tsx +472 -193
  44. package/src/routes/pages/messaging.tsx +1 -1
  45. package/src/server/lib/dispatch-integrations.spec.ts +113 -0
  46. package/src/server/lib/dispatch-integrations.ts +239 -6
  47. package/src/server/lib/dispatch-routing.spec.ts +42 -0
  48. package/src/server/lib/dispatch-routing.ts +42 -0
  49. package/src/server/lib/env-config.ts +50 -0
  50. package/src/server/lib/onboarding-steps.ts +32 -0
  51. package/src/server/plugins/integrations.ts +5 -2
@@ -7,6 +7,7 @@ import {
7
7
  DISPATCH_APP_ID,
8
8
  executeProviderApiRequest,
9
9
  } from "../server/lib/provider-api.js";
10
+ import { buildProviderApiAuditSummary } from "./provider-api-audit.js";
10
11
 
11
12
  const MethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
12
13
 
@@ -197,6 +198,15 @@ export default defineAction({
197
198
  ),
198
199
  }),
199
200
  http: false,
201
+ audit: {
202
+ recordInputs: false,
203
+ target: (args) => ({
204
+ type: "provider-api",
205
+ id: String(args.provider),
206
+ visibility: "private",
207
+ }),
208
+ summary: (args) => buildProviderApiAuditSummary(args),
209
+ },
200
210
  run: async (args) => {
201
211
  if (args.stageAs) {
202
212
  const ctx = getCredentialContext();
@@ -0,0 +1,108 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ getInstallation: vi.fn(),
5
+ resolveTokenBundle: vi.fn(),
6
+ resolveSecret: vi.fn(),
7
+ sendMessageToTarget: vi.fn(),
8
+ getDestinationById: vi.fn(),
9
+ slackAdapter: vi.fn(),
10
+ }));
11
+
12
+ vi.mock("@agent-native/core/integrations", () => ({
13
+ listIntegrationInstallations: mocks.getInstallation,
14
+ resolveIntegrationTokenBundle: mocks.resolveTokenBundle,
15
+ }));
16
+
17
+ vi.mock("@agent-native/core/server", () => ({
18
+ getRequestUserEmail: () => "member@example.com",
19
+ getRequestOrgId: () => "org-a",
20
+ resolveSecret: mocks.resolveSecret,
21
+ isEmailConfigured: vi.fn(),
22
+ slackAdapter: mocks.slackAdapter,
23
+ telegramAdapter: vi.fn(),
24
+ emailAdapter: vi.fn(),
25
+ }));
26
+
27
+ vi.mock("../server/lib/dispatch-store.js", () => ({
28
+ getDestinationById: mocks.getDestinationById,
29
+ }));
30
+
31
+ const action = (await import("./send-platform-message.js")).default;
32
+
33
+ describe("send-platform-message tenant scoping", () => {
34
+ beforeEach(() => {
35
+ vi.clearAllMocks();
36
+ mocks.getDestinationById.mockResolvedValue(null);
37
+ mocks.resolveSecret.mockResolvedValue(null);
38
+ mocks.slackAdapter.mockImplementation(
39
+ (options: { resolveBotToken: () => Promise<string | undefined> }) => ({
40
+ formatAgentResponse: (text: string) => ({
41
+ text,
42
+ platformContext: {},
43
+ }),
44
+ sendMessageToTarget: async (...args: unknown[]) => {
45
+ expect(await options.resolveBotToken()).toBe("own-managed-token");
46
+ return mocks.sendMessageToTarget(...args);
47
+ },
48
+ }),
49
+ );
50
+ });
51
+
52
+ it("resolves and uses only the caller org's Slack installation", async () => {
53
+ mocks.getInstallation.mockResolvedValue([
54
+ {
55
+ id: "installation-a",
56
+ installationKey: "team:T1:app:A1",
57
+ teamId: "T1",
58
+ enterpriseId: null,
59
+ },
60
+ ]);
61
+ mocks.resolveTokenBundle.mockResolvedValue({
62
+ accessToken: "own-managed-token",
63
+ });
64
+
65
+ await action.run(
66
+ {
67
+ platform: "slack",
68
+ destination: "C1",
69
+ tenantId: "T1",
70
+ text: "hello",
71
+ },
72
+ {} as never,
73
+ );
74
+
75
+ expect(mocks.getInstallation).toHaveBeenCalledWith(
76
+ {
77
+ userEmail: "member@example.com",
78
+ orgId: "org-a",
79
+ },
80
+ "slack",
81
+ );
82
+ expect(mocks.resolveTokenBundle).toHaveBeenCalledWith(
83
+ "slack",
84
+ "team:T1:app:A1",
85
+ );
86
+ expect(mocks.sendMessageToTarget).toHaveBeenCalledOnce();
87
+ });
88
+
89
+ it("does not use another org's managed installation", async () => {
90
+ mocks.getInstallation.mockResolvedValue([]);
91
+
92
+ await expect(
93
+ action.run(
94
+ {
95
+ platform: "slack",
96
+ destination: "C1",
97
+ tenantId: "T-other",
98
+ text: "hello",
99
+ },
100
+ {} as never,
101
+ ),
102
+ ).rejects.toThrow("That Slack workspace is not connected");
103
+
104
+ expect(mocks.resolveTokenBundle).not.toHaveBeenCalled();
105
+ expect(mocks.slackAdapter).not.toHaveBeenCalled();
106
+ expect(mocks.sendMessageToTarget).not.toHaveBeenCalled();
107
+ });
108
+ });
@@ -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 {
@@ -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
+ });