@agent-native/dispatch 0.38.11 → 0.38.12-nightly-20260926081002

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.38.11",
3
+ "version": "0.38.12-nightly-20260926081002",
4
4
  "description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -88,7 +88,7 @@
88
88
  "tailwind-merge": "^3.5.0",
89
89
  "vaul": "^1.1.2",
90
90
  "zod": "^4.3.6",
91
- "@agent-native/toolkit": "0.21.3"
91
+ "@agent-native/toolkit": "0.22.0-nightly-20260926081002"
92
92
  },
93
93
  "devDependencies": {
94
94
  "@react-router/dev": "^8.1.0",
@@ -102,10 +102,10 @@
102
102
  "typescript-7": "npm:typescript@^7.0.2",
103
103
  "vite": "8.1.0",
104
104
  "vitest": "^4.1.5",
105
- "@agent-native/core": "0.191.0"
105
+ "@agent-native/core": "0.192.0-nightly-20260926081002"
106
106
  },
107
107
  "peerDependencies": {
108
- "@agent-native/core": ">=0.8.0",
108
+ "@agent-native/core": "0.192.0-nightly-20260926081002",
109
109
  "react": ">=19.2.7",
110
110
  "react-dom": ">=19.2.7",
111
111
  "react-router": ">=8"
@@ -0,0 +1,78 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ listWorkspaceApps: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("../server/lib/app-creation-store.js", () => ({
8
+ listWorkspaceApps: mocks.listWorkspaceApps,
9
+ }));
10
+
11
+ import type { ActionRunContext } from "@agent-native/core/action";
12
+
13
+ import action from "./list-workspace-apps.js";
14
+
15
+ const input = {
16
+ includeAgentCards: false,
17
+ includeArchived: false,
18
+ audience: "all" as const,
19
+ };
20
+
21
+ function context(userEmail: string, orgId: string | null): ActionRunContext {
22
+ return {
23
+ caller: "http",
24
+ userEmail,
25
+ orgId,
26
+ appId: "dispatch",
27
+ };
28
+ }
29
+
30
+ describe("list-workspace-apps", () => {
31
+ beforeEach(() => {
32
+ vi.clearAllMocks();
33
+ });
34
+
35
+ it("shares only a pending registry read for the same authorization context", async () => {
36
+ mocks.listWorkspaceApps.mockResolvedValue([]);
37
+ let resolve!: (apps: never[]) => void;
38
+ mocks.listWorkspaceApps.mockImplementationOnce(
39
+ () =>
40
+ new Promise((done) => {
41
+ resolve = done;
42
+ }),
43
+ );
44
+
45
+ const caller = context("member@example.com", "org-1");
46
+ const first = action.run(input, caller);
47
+ const concurrent = action.run(input, caller);
48
+ await vi.waitFor(() =>
49
+ expect(mocks.listWorkspaceApps).toHaveBeenCalledOnce(),
50
+ );
51
+
52
+ resolve([]);
53
+ await expect(Promise.all([first, concurrent])).resolves.toEqual([[], []]);
54
+
55
+ await action.run(input, caller);
56
+ expect(mocks.listWorkspaceApps).toHaveBeenCalledTimes(2);
57
+ });
58
+
59
+ it("isolates users, organizations, app contexts, and list options", async () => {
60
+ mocks.listWorkspaceApps.mockResolvedValue([]);
61
+
62
+ await Promise.all([
63
+ action.run(input, context("member@example.com", "org-1")),
64
+ action.run(input, context("another@example.com", "org-1")),
65
+ action.run(input, context("member@example.com", "org-2")),
66
+ action.run(input, {
67
+ ...context("member@example.com", "org-1"),
68
+ appId: "other-dispatch",
69
+ }),
70
+ action.run(
71
+ { ...input, includeArchived: true },
72
+ context("member@example.com", "org-1"),
73
+ ),
74
+ ]);
75
+
76
+ expect(mocks.listWorkspaceApps).toHaveBeenCalledTimes(5);
77
+ });
78
+ });
@@ -1,7 +1,44 @@
1
- import { defineAction } from "@agent-native/core/action";
1
+ import { defineAction, type ActionRunContext } from "@agent-native/core/action";
2
2
  import { z } from "zod";
3
3
 
4
- import { listWorkspaceApps } from "../server/lib/app-creation-store.js";
4
+ import {
5
+ listWorkspaceApps,
6
+ type ListWorkspaceAppsOptions,
7
+ type WorkspaceAppSummary,
8
+ } from "../server/lib/app-creation-store.js";
9
+
10
+ const inFlightWorkspaceAppLists = new Map<
11
+ string,
12
+ Promise<WorkspaceAppSummary[]>
13
+ >();
14
+
15
+ function listWorkspaceAppsForRequest(
16
+ input: ListWorkspaceAppsOptions,
17
+ context?: ActionRunContext,
18
+ ): Promise<WorkspaceAppSummary[]> {
19
+ const email = context?.userEmail?.trim().toLowerCase();
20
+ if (!context || !email) return listWorkspaceApps(input);
21
+
22
+ const key = JSON.stringify([
23
+ email,
24
+ context.orgId?.trim() || null,
25
+ context.appId?.trim() || null,
26
+ context.caller,
27
+ input,
28
+ ]);
29
+ const pending = inFlightWorkspaceAppLists.get(key);
30
+ if (pending) return pending;
31
+
32
+ const request = listWorkspaceApps(input);
33
+ inFlightWorkspaceAppLists.set(key, request);
34
+ const removeSettledRequest = () => {
35
+ if (inFlightWorkspaceAppLists.get(key) === request) {
36
+ inFlightWorkspaceAppLists.delete(key);
37
+ }
38
+ };
39
+ void request.then(removeSettledRequest, removeSettledRequest);
40
+ return request;
41
+ }
5
42
 
6
43
  const httpBoolean = z.preprocess((value) => {
7
44
  if (typeof value !== "string") return value;
@@ -29,5 +66,5 @@ export default defineAction({
29
66
  .describe("Filter by workspace app audience."),
30
67
  }),
31
68
  http: { method: "GET" },
32
- run: async (input) => listWorkspaceApps(input),
69
+ run: async (input, context) => listWorkspaceAppsForRequest(input, context),
33
70
  });
@@ -45,7 +45,9 @@ const mocks = vi.hoisted(() => {
45
45
  settings.set(key, value);
46
46
  }),
47
47
  getOrgSetting: vi.fn(async () => null),
48
- isWorkspaceAppAccessAllowed: vi.fn(async () => true),
48
+ isWorkspaceAppAccessAllowed: vi.fn(
49
+ async (): Promise<boolean | "unavailable"> => true,
50
+ ),
49
51
  resolveAccess: vi.fn(async () => ({
50
52
  role: "viewer",
51
53
  resource: {},
@@ -798,6 +800,18 @@ describe("listWorkspaceApps", () => {
798
800
  });
799
801
  });
800
802
 
803
+ it("does not expose Dispatch when its access check is unavailable", async () => {
804
+ stubManifest();
805
+ mocks.isWorkspaceAppAccessAllowed.mockResolvedValueOnce("unavailable");
806
+
807
+ const apps = await runWithRequestContext(
808
+ { userEmail: "member@example.test", orgId: "org-123" },
809
+ () => listWorkspaceApps({ includeAgentCards: false }),
810
+ );
811
+
812
+ expect(apps).toEqual([]);
813
+ });
814
+
801
815
  it("does not expose the workspace app registry without an authenticated user", async () => {
802
816
  stubNoPendingContext();
803
817
  stubManifest([
@@ -1556,12 +1556,11 @@ async function filterWorkspaceAppsByAccess(
1556
1556
  continue;
1557
1557
  }
1558
1558
  if (app.isDispatch) {
1559
- if (
1560
- await isWorkspaceAppAccessAllowed("dispatch", {
1561
- email: userEmail,
1562
- orgId,
1563
- })
1564
- ) {
1559
+ const dispatchAccess = await isWorkspaceAppAccessAllowed("dispatch", {
1560
+ email: userEmail,
1561
+ orgId,
1562
+ });
1563
+ if (dispatchAccess === true) {
1565
1564
  visibleIds.add(app.id);
1566
1565
  }
1567
1566
  continue;